├── .github
└── ISSUE_TEMPLATE
│ ├── bugreport.yml
│ └── config.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── VERSION
├── biome.json
├── electron
├── main.ts
└── preload.ts
├── image
├── features.png
├── help.png
└── main.png
├── package-lock.json
├── package.json
├── public
├── css
│ └── styles.css
├── icon
│ ├── download.svg
│ └── x_logo.svg
├── index.html
└── js
│ ├── core.js
│ ├── date-range-handler.js
│ ├── network-controls.js
│ ├── network-generation.js
│ └── network-visualization.js
├── src
├── app.ts
├── controllers
│ ├── imageController.ts
│ └── metadataController.ts
├── endpoints
│ ├── configDirectoryEndpoint.ts
│ ├── imageUploadEndpoint.ts
│ ├── metadataDateRangeEndpoint.ts
│ ├── metadataFileEndpoint.ts
│ ├── metadataFilesEndpoint.ts
│ ├── metadataFilterEndpoint.ts
│ ├── metadataGenerationEndpoint.ts
│ ├── metadataStopEndpoint.ts
│ └── serverShutdownEndpoint.ts
├── routes
│ └── apiRoutes.ts
├── services
│ ├── fileStorageService.ts
│ ├── imageService.ts
│ └── metadataService.ts
├── types.ts
└── utils
│ ├── errorHandler.ts
│ └── pngParser.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bugreport.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report to help us improve the VRChat Friend Network visualization tool.
3 | labels: ["bug", "triage"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | ### 🐞 Bug reports help improve the VRChat Friend Network visualization tool!
9 |
10 | * ❔ For questions, help, ideas, or feature requests, please use the [Discussions](https://github.com/refiaa/VRChatFriendShipVisualizer/discussions) tab.
11 |
12 | * Before submitting a bug report, please search [existing issues](https://github.com/refiaa/VRChatFriendShipVisualizer/issues) to avoid duplicates.
13 |
14 | * Please provide detailed information to help us understand and resolve the issue efficiently.
15 | - type: input
16 | attributes:
17 | label: VRChat Friend Network Version
18 | description: Which version of the application are you using? Check package.json or git commit hash.
19 | placeholder: "v1.0.0 or commit hash"
20 | validations:
21 | required: true
22 | - type: input
23 | attributes:
24 | label: Node.js Version
25 | description: Output of `node --version`
26 | placeholder: "v16.14.0"
27 | validations:
28 | required: true
29 | - type: input
30 | attributes:
31 | label: Operating System
32 | description: Which OS are you using?
33 | placeholder: "Windows 10, Ubuntu 20.04, macOS Monterey"
34 | validations:
35 | required: true
36 | - type: input
37 | attributes:
38 | label: Browser & Version
39 | description: Which browser and version are you using to view the visualization?
40 | placeholder: "Chrome 120.0.6099.109"
41 | validations:
42 | required: true
43 | - type: textarea
44 | attributes:
45 | label: Reproduction Steps
46 | description: How can we reproduce this issue? Include exact steps.
47 | placeholder: |
48 | 1. Start the server with `npm run dev`
49 | 2. Navigate to 'http://localhost:3000'
50 | 3. Upload images to './img' folder
51 | 4. Click on '...'
52 | 5. Observe error
53 | validations:
54 | required: true
55 | - type: textarea
56 | attributes:
57 | label: Expected Behavior
58 | description: What did you expect to happen?
59 | placeholder: "The graph should display connections between players..."
60 | validations:
61 | required: true
62 | - type: textarea
63 | attributes:
64 | label: Actual Behavior
65 | description: What actually happened? Include any error messages or unexpected behavior.
66 | placeholder: "The graph failed to render and console showed errors..."
67 | validations:
68 | required: true
69 | - type: textarea
70 | attributes:
71 | label: Server Logs
72 | description: Include relevant server logs (from terminal where you run npm run dev)
73 | placeholder: "Paste server logs here..."
74 | validations:
75 | required: false
76 | - type: textarea
77 | attributes:
78 | label: Browser Console Errors
79 | description: Include any relevant browser console errors (F12 > Console)
80 | placeholder: "Paste browser console errors here..."
81 | validations:
82 | required: false
83 | - type: textarea
84 | attributes:
85 | label: Image Processing Details
86 | description: If the issue is related to image processing, provide details about the images
87 | placeholder: |
88 | - Number of images:
89 | - Image format(s):
90 | - Were images taken with VRCX running?
91 | - Screenshot helper status:
92 | validations:
93 | required: false
94 | - type: textarea
95 | attributes:
96 | label: Additional Context
97 | description: Add any other context about the problem here. **Ensure no personal information or VRChat usernames are included.**
98 | validations:
99 | required: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false # Disables the ability to open blank issues
2 | contact_links:
3 | - name: 🤷♀️ Questions or need help?
4 | url: https://github.com/refiaa/VRChatFriendShipVisualizer/discussions/categories/questions
5 | about: If you have questions or need help with VRChatFriendShipVisualizer, check existing questions or post a new one here.
6 |
7 | - name: 💬 Discussions
8 | url: https://github.com/refiaa/VRChatFriendShipVisualizer/discussions
9 | about: Join the discussions. Share your experiences, challenges, and workarounds with the VRChatFriendShipVisualizer community.
10 |
11 | - name: 💡 Suggestions / Ideas
12 | url: https://github.com/refiaa/VRChatFriendShipVisualizer/discussions/categories/ideas
13 | about: Have ideas for new features or improvements? Check out existing suggestions or share your own.
14 |
15 | issue_templates:
16 | - name: Bug Report
17 | description: Report a bug to help us improve VRChatFriendShipVisualizer.
18 | template: bugreport.yml
19 | title: "[BUG] "
20 | labels: ["bug", "needs triage"]
21 |
22 | - name: Feature Request
23 | description: Suggest an idea or enhancement for VRChatFriendShipVisualizer.
24 | template: feature_request.yml
25 | title: "[FEATURE] "
26 | labels: ["enhancement", "needs review"]
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
3 |
4 | # User-specific stuff
5 | .idea/**/workspace.xml
6 | .idea/**/tasks.xml
7 | .idea/**/usage.statistics.xml
8 | .idea/**/dictionaries
9 | .idea/**/shelf
10 |
11 | # AWS User-specific
12 | .idea/**/aws.xml
13 |
14 | # Generated files
15 | .idea/**/contentModel.xml
16 |
17 | # Sensitive or high-churn files
18 | .idea/**/dataSources/
19 | .idea/**/dataSources.ids
20 | .idea/**/dataSources.local.xml
21 | .idea/**/sqlDataSources.xml
22 | .idea/**/dynamic.xml
23 | .idea/**/uiDesigner.xml
24 | .idea/**/dbnavigator.xml
25 |
26 | # Gradle
27 | .idea/**/gradle.xml
28 | .idea/**/libraries
29 |
30 | # Gradle and Maven with auto-import
31 | # When using Gradle or Maven with auto-import, you should exclude module files,
32 | # since they will be recreated, and may cause churn. Uncomment if using
33 | # auto-import.
34 | # .idea/artifacts
35 | # .idea/compiler.xml
36 | # .idea/jarRepositories.xml
37 | # .idea/modules.xml
38 | # .idea/*.iml
39 | # .idea/modules
40 | # *.iml
41 | # *.ipr
42 |
43 | # CMake
44 | cmake-build-*/
45 |
46 | # Mongo Explorer plugin
47 | .idea/**/mongoSettings.xml
48 |
49 | # File-based project format
50 | *.iws
51 |
52 | # IntelliJ
53 | out/
54 |
55 | # mpeltonen/sbt-idea plugin
56 | .idea_modules/
57 |
58 | # JIRA plugin
59 | atlassian-ide-plugin.xml
60 |
61 | # Cursive Clojure plugin
62 | .idea/replstate.xml
63 |
64 | # SonarLint plugin
65 | .idea/sonarlint/
66 |
67 | # Crashlytics plugin (for Android Studio and IntelliJ)
68 | com_crashlytics_export_strings.xml
69 | crashlytics.properties
70 | crashlytics-build.properties
71 | fabric.properties
72 |
73 | # Editor-based Rest Client
74 | .idea/httpRequests
75 |
76 | # Android studio 3.1+ serialized cache file
77 | .idea/caches/build_file_checksums.ser
78 |
79 | node_modules/
80 | data/metadata/
81 | .env
82 | .DS_Store
83 | data/
84 | img/
85 |
86 | .idea/
87 | dist/
88 | public/uploads
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.0.1 - 2024-12-12
4 | ### New features
5 | - Basic VRChat friendship visualization functionality
6 | - Metadata generation and processing system
7 | - Network graph visualization (using D3.js)
8 | - Friendship strength-based visualization
9 | - Search and highlight functionality
10 | - SVG export feature
11 | - X(Twitter) share feature
12 | - Directory configuration functionality
13 |
14 | ### Changes
15 | - Migrated JavaScript backend to TypeScript
16 | - Implemented Express.js based RESTful API
17 |
18 | ## 0.0.2 - 2024-12-13
19 | ### New features
20 | - Added placeholder text for initial page load and no data scenarios
21 | - Improved user guidance with welcome message when first loading the page
22 | - Enhanced empty state handling throughout the visualization process
23 |
24 | ### Bugfixes
25 | - Corrected event listener timing for search functionality
26 |
27 | ## 0.0.3 - 2024-12-13
28 | ### New features
29 | - Added date range selection using a slider interface
30 |
31 | ### Changes
32 | - Improved structure of ./public/js directory
33 |
34 | ### Bugfixes
35 | - Modified backend API to enable filter application even when metadata files exist without internal data
36 |
37 | ## 0.0.3.1 - 2024-12-13
38 | ### Bugfixes
39 | - Fixed an issue where slider values would reset after applying date filter
40 | - Fixed an issue where sliders were still movable when start and end months were the same
41 |
42 | ## 0.0.3.2 - 2024-12-18
43 | ### Changes
44 | - Enhanced node filtering logic to better handle circular references
45 | - Modified network visualization Logic for better performance
46 | - Backend / Frontend Structure Refactored
47 |
48 | ### Bugfixes
49 | - Fixed search functionality after recent refactoring
50 |
51 | ## 0.0.3.3 - 2024-12-28
52 | ### Changes
53 | - Modified default image directory path to use user's VRChat folder
54 |
55 | ## 0.0.3.4 - 2024-12-28
56 | ### New features
57 | - Added server shutdown functionality
58 | - Added shutdown server button to UI
59 | - Implemented clean server shutdown endpoint
60 |
61 | ---
62 |
63 | ## 0.1.0 - 2025-02-03
64 |
65 | ### New Features
66 | - **EXE Packaging**
67 |
68 | - **Backend Enhancements**
69 | - Changed the metadata storage directory to the user's Pictures folder under `VRChat\metadata` so that metadata JSON files are generated in a familiar and accessible location (e.g., `C:\Users\{username}\Pictures\VRChat\metadata`).
70 | - Refactored file path and environment variable handling in backend APIs to improve stability and error handling.
71 |
72 | ### Changes
73 | - Optimized the list of files and dependencies included in the final bundle to exclude unnecessary development files and resources.
74 | - Updated NSIS installer configuration to allow users to change the installation directory and to create shortcuts on the desktop and in the Start Menu.
75 | - Refactored metadata directory initialization to correctly handle existing files versus directories, ensuring that the metadata directory is properly created even in a packaged environment.
76 |
77 | ### Bugfixes
78 | - Fixed an issue where the metadata directory initialization would fail with an ENOTDIR error by changing the metadata directory to a more appropriate location (under `Pictures\VRChat\metadata`).
79 | - Addressed minor API and UI issues, including proper handling of environment variables and file paths in the backend.
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-2025 Refia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # VRChat Friend Network
4 |
5 |
6 | [](https://github.com/refiaa/VRChatFriendShipVisualizer/blob/master/LICENSE)
7 | [](https://github.com/refiaa/VRChatFriendShipVisualizer/releases/latest)
8 | [](https://github.com/refiaa/VRChatFriendShipVisualizer/stargazers)
9 | [](https://github.com/refiaa/VRChatFriendShipVisualizer/network/members)
10 | [](https://github.com/refiaa/VRChatFriendShipVisualizer/pulls?q=is%3Apr+is%3Aclosed)
11 | [](https://github.com/refiaa/VRChatFriendShipVisualizer/issues?q=is%3Aissue+is%3Aclosed)
12 |
13 |
14 | 
15 |
16 |
17 |
18 | > [!WARNING]
19 | > Since this installer is not signed with a paid software certificate, a warning may appear when launching it.
20 | > If you wish to proceed, click “More info” and then select “Run anyway.”
21 |
22 |
23 | A web-based application that visualizes your VRChat friend network using interactive D3.js graphs. Easily scan your VRChat photos, generate metadata, and explore connections between players through an intuitive and dynamic interface.
24 |
25 | Only photos taken while [**VRCX**](https://github.com/vrcx-team/VRCX) is running are supported. In addition, only photos taken with the `screenshot helper` **enabled** in VRCX can be used.
26 |
27 |
28 |
29 | 
30 |
31 |
32 |
33 |
34 |
35 | ---
36 |
37 | - [Features](#features)
38 | - [Installation](#installation)
39 | - [Installation as a Standalone EXE](#installation-as-a-standalone-exe)
40 | - [Installation via Node.js (for dev)](#installation-via-nodejs)
41 | - [Setup](#setup)
42 | - [Usage](#usage)
43 | - [Dependencies](#dependencies)
44 | - [VirusTotal Scan](#virustotal-scan)
45 | - [Changelog](#changelog)
46 | - [License](#license)
47 |
48 | ## Features
49 |
50 | 
51 |
52 | - **Interactive Network Graph:** Visualize connections between VRChat players with dynamic force-directed graphs.
53 | - **Search Functionality:** Easily search for players by name and highlight their connections.
54 | - **Responsive Design:** Accessible and functional across various screen sizes.
55 | - **Share and Download:** Download your network visualization as an SVG or share directly to Twitter using tmpfiles.org.
56 | - **Date Range-based Data Filter:** Filter your images by date range using an intuitive slider.
57 | - **EXE Installation Package:** (New) Install the application as a standalone Windows program with typical installer features (default installation folder, desktop and start menu shortcuts).
58 |
59 | ## Installation
60 |
61 |
62 | ### Installation as a Standalone EXE
63 |
64 | For users who prefer a traditional desktop installation, a standalone EXE package is available.
65 |
66 | 1. **Download the Installer:**
67 | - Visit the [GitHub Releases](https://github.com/refiaa/VRChatFriendShipVisualizer/releases) page and download the latest installer (EXE).
68 |
69 | 2. **Run the Installer:**
70 | - Double-click the installer.
71 | - Follow the on-screen instructions to install the application (default installation directory is typically `C:\Program Files\VRChatFriendShipVisualizer`).
72 | - During installation, you can change the installation directory if desired.
73 | - The installer will automatically create desktop and Start Menu shortcuts.
74 |
75 | 3. **Run the Application:**
76 | - After installation, launch the app either from the desktop shortcut or from the Start Menu.
77 | - The application will run as a standalone desktop application with an embedded Express server and all features intact.
78 |
79 |
80 | ### Installation via Node.js
81 |
82 | #### Prerequisites
83 |
84 | Before you begin, ensure you have met the following requirements:
85 |
86 | - **Node.js:** Install Node.js (v14 or later) from [Node.js official website](https://nodejs.org/).
87 | - **npm:** Node.js installation includes npm. Verify installation by running:
88 | ```bash
89 | node -v
90 | npm -v
91 | ```
92 |
93 | 1. **Clone the Repository:**
94 | ```bash
95 | git clone https://github.com/refiaa/VRChatFriendShipVisualizer.git
96 | ```
97 |
98 | 2. **Navigate to the Project Directory:**
99 | ```bash
100 | cd VRChatFriendShipVisualizer
101 | ```
102 |
103 | 3. **Install Dependencies:**
104 | ```bash
105 | npm install
106 | ```
107 |
108 | 4. **Run the Application:**
109 | ```bash
110 | npm run dev
111 | ```
112 | - The development server will start on the configured port (default is 3000).
113 | - Open your browser and navigate to [http://localhost:3000](http://localhost:3000/) to view the application.
114 |
115 | ## Setup
116 |
117 | 1. **Prepare VRChat Images:**
118 | - By default, the application uses your VRChat image folder located at:
119 | ```
120 | C:\Users\{YourUsername}\Pictures\VRChat
121 | ```
122 | - To change the directory, use the directory configuration option in the app UI.
123 |
124 | 2. **Metadata Generation:**
125 | - Metadata is generated from your VRChat photos and stored in the `metadata` folder located inside your VRChat folder:
126 | ```
127 | C:\Users\{YourUsername}\Pictures\VRChat\metadata
128 | ```
129 |
130 | ## Usage
131 |
132 | 1. **Start the Application:**
133 | - For Node.js version, run `npm run dev` and navigate to [http://localhost:3000](http://localhost:3000/).
134 | - For the EXE version, simply launch the installed application.
135 |
136 | 2. **Update Visualization:**
137 | - Place your VRChat photos in the designated folder or configure a new directory.
138 | - Click on **Update Visualization** to generate the friend network graph.
139 |
140 | 3. **Additional Features:**
141 | - Use the search functionality to highlight specific nodes.
142 | - Filter data by date range using the slider.
143 | - Export the visualization as an SVG or share via Twitter.
144 |
145 | ## Dependencies
146 |
147 | - **D3.js:** Used for dynamic, interactive network graph visualizations.
148 | - **Express.js:** Provides a RESTful API and serves the web application.
149 | - **Electron:** Packages the application as a standalone desktop app.
150 | - **Additional Libraries:** form-data, fs-extra, node-fetch, and others as listed in package.json.
151 |
152 | ## VirusTotal Scan
153 |
154 | This executable package has been scanned on [VirusTotal](https://www.virustotal.com/gui/file/f6864c0f5ca58c3448dc0800209a7fc2f6244e887dceecbffb27bec97861eb08) and is reported clean by most antivirus engines. Minor detections (e.g., Bkav Pro) are known false positives common with Electron applications.
155 |
156 |
157 | ## Changelog
158 |
159 | See [CHANGELOG.md](CHANGELOG.md) for a list of notable changes and updates.
160 |
161 | ## License
162 |
163 | This project is licensed under the [MIT License](LICENSE).
164 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.1
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "formatter": {
4 | "enabled": true,
5 | "indentStyle": "space",
6 | "indentWidth": 2,
7 | "lineWidth": 100
8 | },
9 | "linter": {
10 | "enabled": true,
11 | "rules": {
12 | "recommended": true,
13 | "complexity": {
14 | "all": true
15 | },
16 | "style": {
17 | "all": true,
18 | "useNamingConvention": {
19 | "level": "warn",
20 | "options": {
21 | "strictCase": false
22 | }
23 | }
24 | },
25 | "suspicious": {
26 | "noExplicitAny": {
27 | "level": "off"
28 | }
29 | }
30 | }
31 | },
32 | "organizeImports": {
33 | "enabled": true
34 | },
35 | "files": {
36 | "ignore": ["node_modules/", "dist/", "public/"]
37 | },
38 | "vcs": {
39 | "enabled": true,
40 | "clientKind": "git",
41 | "useIgnoreFile": true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/electron/main.ts:
--------------------------------------------------------------------------------
1 | import { app as electronApp, BrowserWindow } from 'electron';
2 | import * as path from 'path';
3 | import { server } from '../src/app';
4 |
5 | let mainWindow: BrowserWindow | null = null;
6 |
7 | function createMainWindow(): void {
8 | mainWindow = new BrowserWindow({
9 | width: 1200,
10 | height: 800,
11 | webPreferences: {
12 | preload: path.join(__dirname, 'preload.js'),
13 | contextIsolation: true,
14 | nodeIntegration: false
15 | }
16 | });
17 |
18 | getServerPort()
19 | .then((port) => {
20 | mainWindow!.loadURL(`http://localhost:${port}`);
21 | })
22 | .catch((error) => {
23 | console.error("Failed to get server port:", error);
24 | });
25 |
26 | mainWindow.on('closed', () => {
27 | mainWindow = null;
28 | });
29 | }
30 |
31 | function getServerPort(): Promise
{
32 | return new Promise((resolve, reject) => {
33 | if (server.listening) {
34 | const address = server.address();
35 | if (address && typeof address === "object") {
36 | resolve(address.port);
37 | } else {
38 | reject(new Error("Server address is not an object"));
39 | }
40 | } else {
41 | server.on("listening", () => {
42 | const address = server.address();
43 | if (address && typeof address === "object") {
44 | resolve(address.port);
45 | } else {
46 | reject(new Error("Server address is not an object"));
47 | }
48 | });
49 | }
50 | });
51 | }
52 |
53 | electronApp.whenReady().then(() => {
54 | createMainWindow();
55 | electronApp.on('activate', () => {
56 | if (BrowserWindow.getAllWindows().length === 0) {
57 | createMainWindow();
58 | }
59 | });
60 | });
61 |
62 | electronApp.on('window-all-closed', () => {
63 | if (process.platform !== 'darwin') {
64 | server.close(() => {
65 | electronApp.quit();
66 | });
67 | }
68 | });
69 |
--------------------------------------------------------------------------------
/electron/preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from 'electron';
2 |
3 | contextBridge.exposeInMainWorld('electronAPI', {});
4 |
--------------------------------------------------------------------------------
/image/features.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refiaa/VRChatFriendShipVisualizer/49c0141c68a57dee2b58296af0da36973ebd8651/image/features.png
--------------------------------------------------------------------------------
/image/help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refiaa/VRChatFriendShipVisualizer/49c0141c68a57dee2b58296af0da36973ebd8651/image/help.png
--------------------------------------------------------------------------------
/image/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refiaa/VRChatFriendShipVisualizer/49c0141c68a57dee2b58296af0da36973ebd8651/image/main.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vrchatfriendshipvisualizer",
3 | "version": "0.1.0",
4 | "description": "VRChat friendship network visualization tool",
5 | "main": "dist/electron/main.js",
6 | "scripts": {
7 | "start": "electron .",
8 | "dev": "nodemon --exec ts-node src/app.ts --ignore 'data/*' --ignore 'public/*'",
9 | "build": "tsc && npm run copy-public && npm run copy-version",
10 | "copy-public": "copyfiles -u 1 public/**/* dist/public",
11 | "copy-version": "copyfiles -u 0 VERSION dist/",
12 | "format": "npx biome format --write .",
13 | "lint": "npx biome lint .",
14 | "electron:build": "cross-env WIN_CSC_LINK=\"\" CSC_LINK=\"\" CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder"
15 | },
16 | "keywords": [
17 | "vrchat",
18 | "network",
19 | "visualization"
20 | ],
21 | "author": "",
22 | "license": "ISC",
23 | "dependencies": {
24 | "dotenv": "^16.4.7",
25 | "express": "^4.21.2",
26 | "form-data": "^4.0.1",
27 | "fs-extra": "^11.2.0",
28 | "node-fetch": "^2.7.0"
29 | },
30 | "devDependencies": {
31 | "@biomejs/biome": "^1.9.4",
32 | "@types/d3": "^7.4.3",
33 | "@types/electron": "^1.6.12",
34 | "@types/express": "^5.0.0",
35 | "@types/form-data": "^2.5.2",
36 | "@types/fs-extra": "^11.0.4",
37 | "@types/jest": "^29.5.14",
38 | "@types/node": "^22.10.2",
39 | "@types/node-fetch": "^2.6.12",
40 | "concurrently": "^9.1.2",
41 | "copyfiles": "^2.4.1",
42 | "cross-env": "^7.0.3",
43 | "electron": "^34.0.2",
44 | "electron-builder": "^25.1.8",
45 | "globals": "^15.13.0",
46 | "nodemon": "^3.1.7",
47 | "ts-node": "^10.9.2",
48 | "typescript": "^5.7.2"
49 | },
50 | "nodemonConfig": {
51 | "ignore": [
52 | "data/*",
53 | "public/*",
54 | "*.json"
55 | ],
56 | "watch": [
57 | "src/",
58 | "electron/"
59 | ],
60 | "ext": "js,ts"
61 | },
62 | "build": {
63 | "asar": true,
64 | "appId": "refiaa.vrchatfriendshipvisualizer",
65 | "files": [
66 | "dist/**/*",
67 | "public/**/*",
68 | "node_modules/**/*",
69 | "src/**/*"
70 | ],
71 | "directories": {
72 | "buildResources": "build"
73 | },
74 | "extraResources": [
75 | "VERSION",
76 | "LICENSE"
77 | ],
78 | "win": {
79 | "certificateFile": null,
80 | "certificateSubjectName": null,
81 | "signAndEditExecutable": false
82 | },
83 | "nsis": {
84 | "oneClick": false,
85 | "allowElevation": true,
86 | "allowToChangeInstallationDirectory": true,
87 | "perMachine": true,
88 | "createDesktopShortcut": true,
89 | "createStartMenuShortcut": true,
90 | "shortcutName": "VRChatFriendShipVisualizer"
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/public/css/styles.css:
--------------------------------------------------------------------------------
1 | /* Base styles */
2 | body {
3 | font-family: Arial, sans-serif;
4 | margin: 0;
5 | padding: 20px;
6 | background-color: #f5f5f5;
7 | }
8 |
9 | .container {
10 | max-width: 1200px;
11 | margin: 0 auto;
12 | background-color: white;
13 | padding: 20px;
14 | border-radius: 8px;
15 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
16 | }
17 |
18 | /* Graph container */
19 | #graph {
20 | width: 100%;
21 | height: 800px;
22 | border: 1px solid #ddd;
23 | border-radius: 4px;
24 | background-color: white;
25 | position: relative;
26 | }
27 |
28 | /* Graph elements */
29 | .node text {
30 | font-size: 12px;
31 | fill: #333;
32 | }
33 |
34 | .link {
35 | stroke: #999;
36 | stroke-opacity: 0.6;
37 | }
38 |
39 | /* Controls */
40 | .controls {
41 | margin-bottom: 20px;
42 | }
43 |
44 | button {
45 | padding: 8px 16px;
46 | margin-right: 10px;
47 | background-color: #4CAF50;
48 | color: white;
49 | border: none;
50 | border-radius: 4px;
51 | cursor: pointer;
52 | }
53 |
54 | button:hover {
55 | background-color: #45a049;
56 | }
57 |
58 | button:disabled {
59 | background-color: #cccccc;
60 | cursor: not-allowed;
61 | }
62 |
63 | /* Tooltip */
64 | .tooltip {
65 | position: absolute;
66 | background-color: rgba(0, 0, 0, 0.8);
67 | color: white;
68 | padding: 8px;
69 | border-radius: 4px;
70 | font-size: 12px;
71 | pointer-events: none;
72 | z-index: 1000;
73 | }
74 |
75 | /* Loading overlay */
76 | .loading-overlay {
77 | position: absolute;
78 | top: 0;
79 | left: 0;
80 | right: 0;
81 | bottom: 0;
82 | background-color: rgba(255, 255, 255, 0.9);
83 | display: flex;
84 | flex-direction: column;
85 | align-items: center;
86 | justify-content: center;
87 | z-index: 1000;
88 | }
89 |
90 | .spinner {
91 | width: 50px;
92 | height: 50px;
93 | border: 5px solid #f3f3f3;
94 | border-top: 5px solid #3498db;
95 | border-radius: 50%;
96 | animation: spin 1s linear infinite;
97 | margin-bottom: 10px;
98 | }
99 |
100 | @keyframes spin {
101 | 0% { transform: rotate(0deg); }
102 | 100% { transform: rotate(360deg); }
103 | }
104 |
105 | /* Progress and status */
106 | .progress-status {
107 | margin-top: 10px;
108 | text-align: center;
109 | color: #333;
110 | font-weight: bold;
111 | white-space: nowrap;
112 | overflow: hidden;
113 | text-overflow: ellipsis;
114 | max-width: 100%;
115 | padding: 0 20px;
116 | }
117 |
118 | #result {
119 | margin-top: 20px;
120 | padding: 15px;
121 | background-color: #f8f9fa;
122 | border-radius: 4px;
123 | border: 1px solid #dee2e6;
124 | }
125 |
126 | /* Debug section */
127 | #debug {
128 | margin-top: 10px;
129 | padding: 10px;
130 | background-color: #f8f9fa;
131 | border-radius: 4px;
132 | font-family: monospace;
133 | font-size: 12px;
134 | }
135 |
136 | /* Search functionality */
137 | .search-container {
138 | margin-bottom: 20px;
139 | display: flex;
140 | align-items: center;
141 | gap: 10px;
142 | }
143 |
144 | .search-input {
145 | padding: 8px 12px;
146 | border: 1px solid #ddd;
147 | border-radius: 4px;
148 | font-size: 14px;
149 | flex-grow: 1;
150 | max-width: 300px;
151 | }
152 |
153 | .search-results {
154 | position: absolute;
155 | background: white;
156 | border: 1px solid #ddd;
157 | border-radius: 4px;
158 | max-height: 200px;
159 | overflow-y: auto;
160 | width: 300px;
161 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
162 | z-index: 1000;
163 | }
164 |
165 | .search-result-item {
166 | padding: 8px 12px;
167 | cursor: pointer;
168 | }
169 |
170 | .search-result-item:hover {
171 | background-color: #f5f5f5;
172 | }
173 |
174 | /* Directory input */
175 | .directory-container {
176 | margin-bottom: 20px;
177 | display: flex;
178 | align-items: center;
179 | gap: 10px;
180 | }
181 |
182 | .directory-input {
183 | padding: 8px 12px;
184 | border: 1px solid #ddd;
185 | border-radius: 4px;
186 | font-size: 14px;
187 | flex-grow: 1;
188 | max-width: 500px;
189 | }
190 |
191 | /* Status indicators */
192 | .directory-status {
193 | margin-top: 5px;
194 | font-size: 12px;
195 | }
196 |
197 | .directory-status.success {
198 | color: #28a745;
199 | }
200 |
201 | .directory-status.error {
202 | color: #dc3545;
203 | }
204 |
205 | .success {
206 | color: #28a745;
207 | }
208 |
209 | .error {
210 | color: #dc3545;
211 | }
212 |
213 | /* Node highlighting */
214 | .highlighted-node circle {
215 | stroke: #ff0000;
216 | stroke-width: 3px;
217 | }
218 |
219 | .dimmed {
220 | opacity: 0.2;
221 | }
222 |
223 | /* Action buttons */
224 | .action-buttons {
225 | display: flex;
226 | justify-content: space-between;
227 | align-items: center;
228 | margin-top: 10px;
229 | }
230 |
231 | .main-buttons {
232 | display: flex;
233 | gap: 10px;
234 | }
235 |
236 | #stopButton {
237 | background-color: #dc3545;
238 | }
239 |
240 | #stopButton:hover {
241 | background-color: #c82333;
242 | }
243 |
244 | #stopButton:disabled {
245 | background-color: #dc354580;
246 | }
247 |
248 | /* Share buttons */
249 | .share-button {
250 | padding: 8px;
251 | background: transparent;
252 | border: none;
253 | cursor: pointer;
254 | }
255 |
256 | .share-buttons {
257 | display: flex;
258 | gap: 8px;
259 | align-items: center;
260 | }
261 |
262 | .x-icon {
263 | width: 40px;
264 | height: 40px;
265 | }
266 |
267 | .icon {
268 | width: 30px;
269 | height: 30px;
270 | }
271 |
272 | /* Loading spinner */
273 | .loading-spinner {
274 | display: inline-block;
275 | width: 20px;
276 | height: 20px;
277 | border: 2px solid rgba(0,0,0,0.1);
278 | border-radius: 50%;
279 | border-top-color: #000;
280 | animation: spin 1s linear infinite;
281 | }
282 |
283 | .version {
284 | font-size: 0.5em;
285 | color: #666;
286 | font-weight: normal;
287 | margin-left: 10px;
288 | vertical-align: middle;
289 | }
290 |
291 | .no-data-placeholder text {
292 | font-family: Arial, sans-serif;
293 | }
294 |
295 | .no-data-placeholder .main-message {
296 | font-size: 24px;
297 | fill: #6c757d;
298 | font-weight: bold;
299 | }
300 |
301 | .no-data-placeholder .sub-message {
302 | font-size: 16px;
303 | fill: #6c757d;
304 | }
305 |
306 | .no-data-placeholder rect {
307 | fill: #f8f9fa;
308 | }
309 |
310 | .collapsible-container {
311 | margin-top: 10px;
312 | border: 1px solid #ddd;
313 | border-radius: 4px;
314 | }
315 |
316 | .collapsible-button {
317 | width: 100%;
318 | text-align: left;
319 | padding: 10px;
320 | background-color: #f8f9fa;
321 | border: none;
322 | color: #333;
323 | cursor: pointer;
324 | display: flex;
325 | justify-content: space-between;
326 | align-items: center;
327 | }
328 |
329 | .collapsible-button:hover {
330 | background-color: #e9ecef;
331 | }
332 |
333 | .collapsible-content {
334 | max-height: 0;
335 | transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
336 | }
337 |
338 | .collapsible-content.active {
339 | max-height: 200px;
340 | border-top: 1px solid #ddd;
341 | }
342 |
343 | .date-slider-container {
344 | padding: 20px;
345 | position: relative;
346 | }
347 |
348 | .date-range-labels {
349 | display: flex;
350 | justify-content: space-between;
351 | margin-bottom: 15px;
352 | color: #333;
353 | font-size: 14px;
354 | font-weight: 500;
355 | padding: 0 10px;
356 | }
357 |
358 | .date-range-labels span {
359 | transition: color 0.2s ease;
360 | }
361 |
362 | .date-range-labels span.active {
363 | color: #4CAF50;
364 | font-weight: 600;
365 | }
366 |
367 | .slider-track {
368 | left: 0;
369 | right: 0;
370 | transform: translateY(-50%);
371 | height: 4px;
372 | width: 100%;
373 | background-color: #ddd;
374 | border-radius: 2px;
375 | }
376 |
377 | .double-range-slider {
378 | position: relative;
379 | height: 40px;
380 | padding: 0 10px;
381 | }
382 |
383 | .double-range-slider input[type="range"] {
384 | -webkit-appearance: none;
385 | appearance: none;
386 | position: absolute;
387 | top: 50%;
388 | transform: translateY(-50%);
389 | left: 0;
390 | width: 100%;
391 | height: 4px;
392 | background: none;
393 | pointer-events: none;
394 | margin: 0;
395 | }
396 |
397 | .double-range-slider input[type="range"]::-webkit-slider-thumb {
398 | -webkit-appearance: none;
399 | appearance: none;
400 | width: 0;
401 | height: 0;
402 | background: transparent;
403 | border-left: 8px solid transparent;
404 | border-right: 8px solid transparent;
405 | border-bottom: 12px solid #4CAF50;
406 | cursor: pointer;
407 | pointer-events: auto;
408 | margin-top: -12px;
409 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
410 | position: relative;
411 | filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
412 | }
413 |
414 | .double-range-slider input[type="range"]::-webkit-slider-thumb:hover {
415 | transform: scale(1.15) translateY(-2px);
416 | border-bottom: 12px solid #45a049;
417 | filter: drop-shadow(0 3px 4px rgba(0,0,0,0.25));
418 | }
419 |
420 | .double-range-slider input[type="range"]:active::-webkit-slider-thumb {
421 | transform: scale(0.95) translateY(1px);
422 | filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2));
423 | }
424 |
425 | .double-range-slider input[type="range"]::-moz-range-thumb {
426 | width: 0;
427 | height: 0;
428 | background: transparent;
429 | border-left: 8px solid transparent;
430 | border-right: 8px solid transparent;
431 | border-bottom: 12px solid #4CAF50;
432 | cursor: pointer;
433 | pointer-events: auto;
434 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
435 | filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
436 | }
437 |
438 | .double-range-slider input[type="range"]::-moz-range-thumb:hover {
439 | transform: scale(1.15) translateY(-2px);
440 | border-bottom: 12px solid #45a049;
441 | filter: drop-shadow(0 3px 4px rgba(0,0,0,0.25));
442 | }
443 |
444 | .double-range-slider input[type="range"]:active::-moz-range-thumb {
445 | transform: scale(0.95) translateY(1px);
446 | filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2));
447 | }
448 |
449 | .date-filter-actions {
450 | display: flex;
451 | justify-content: flex-end;
452 | margin-top: 10px;
453 | }
454 |
455 | .date-filter-actions button {
456 | padding: 5px 15px;
457 | }
458 |
459 | .double-range-slider input[type="range"]:disabled {
460 | cursor: not-allowed;
461 | opacity: 0.5;
462 | }
463 |
464 | .double-range-slider input[type="range"]:disabled::-webkit-slider-thumb {
465 | border-bottom-color: #cccccc;
466 | cursor: not-allowed;
467 | }
468 |
469 | .double-range-slider input[type="range"]:disabled::-moz-range-thumb {
470 | border-bottom-color: #cccccc;
471 | cursor: not-allowed;
472 | }
473 |
474 | .shutdown-button {
475 | background-color: #ff4444;
476 | color: white;
477 | }
478 |
479 | .shutdown-button:hover {
480 | background-color: #cc0000;
481 | }
--------------------------------------------------------------------------------
/public/icon/download.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/icon/x_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | VRChat Friend Network
5 |
6 |
7 |
8 |
9 |
10 |
VRChat Friend Network
11 |
12 |
13 |
17 | Update Directory
18 |
19 |
20 |
25 | Clear
26 |
27 |
28 |
43 |
44 |
45 |
Data Filter ▼
46 |
47 |
48 |
49 | Start Date
50 | End Date
51 |
52 |
57 |
58 | Apply Filter
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
Processing...
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |