mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 19:08:52 +00:00
✨ feat: Initial commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import type {
|
||||
AccountsState,
|
||||
AuthStatus,
|
||||
LoginCredentials,
|
||||
TwoFactorMethod,
|
||||
TwoFactorPayload,
|
||||
} from "./types/auth";
|
||||
import type { SocialSnapshot, UserProfile, UserStatus } from "./types/user";
|
||||
import type { FavoriteWorldFolder, World, WorldSnapshot } from "./types/world";
|
||||
import type { Avatar } from "./types/avatar";
|
||||
import type { RepoStats, StoredEntity } from "./types/repository";
|
||||
import type { AccountSettings, ContentFilterKey, Pending2Fa, RecoveryCode } from "./types/settings";
|
||||
import type { Group } from "./types/group";
|
||||
import type { EnhancementId, EnhancementsSnapshot } from "./types/enhancements";
|
||||
import type { GallerySnapshot, Photo, ThumbCacheStats } from "./types/gallery";
|
||||
import type { AppConfig } from "./types/appConfig";
|
||||
import type { GameStatus } from "./types/game";
|
||||
import type { IpcResult } from "./types/result";
|
||||
import type { CacheEntryInfo, CacheStats, DebugSnapshot, LogEntry, WsEvent } from "./types/debug";
|
||||
|
||||
export interface CacheUpdate {
|
||||
cache: CacheEntryInfo[];
|
||||
stats: CacheStats;
|
||||
}
|
||||
|
||||
export interface IpcRequests {
|
||||
"auth:status": () => IpcResult<AuthStatus>;
|
||||
"auth:login": (creds: LoginCredentials) => IpcResult<AuthStatus>;
|
||||
"auth:verify2fa": (payload: TwoFactorPayload) => IpcResult<AuthStatus>;
|
||||
"auth:logout": () => IpcResult<void>;
|
||||
|
||||
"accounts:list": () => IpcResult<AccountsState>;
|
||||
"accounts:switch": (id: string) => IpcResult<AuthStatus>;
|
||||
"accounts:remove": (id: string) => IpcResult<AccountsState>;
|
||||
|
||||
"user:me": () => IpcResult<UserProfile>;
|
||||
"user:get": (userId: string) => IpcResult<UserProfile>;
|
||||
"user:getByName": (username: string) => IpcResult<UserProfile>;
|
||||
"user:search": (query: string) => IpcResult<UserProfile[]>;
|
||||
|
||||
"friends:list": () => IpcResult<UserProfile[]>;
|
||||
|
||||
"world:byUser": (userId: string) => IpcResult<World[]>;
|
||||
"world:search": (query: string) => IpcResult<World[]>;
|
||||
"world:favorites": (userId: string) => IpcResult<FavoriteWorldFolder[]>;
|
||||
"world:get": (worldId: string) => IpcResult<World>;
|
||||
"world:snapshot": () => IpcResult<WorldSnapshot>;
|
||||
|
||||
"avatar:get": (avatarId: string) => IpcResult<Avatar>;
|
||||
"avatar:favorites": () => IpcResult<Avatar[]>;
|
||||
|
||||
"group:byUser": (userId: string) => IpcResult<Group[]>;
|
||||
"group:represented": (userId: string) => IpcResult<Group | null>;
|
||||
|
||||
"social:snapshot": () => IpcResult<SocialSnapshot>;
|
||||
|
||||
"settings:get": () => IpcResult<AccountSettings>;
|
||||
"settings:displayName": (p: {
|
||||
displayName: string;
|
||||
currentPassword: string;
|
||||
}) => IpcResult<AccountSettings>;
|
||||
"settings:revertDisplayName": (p: { currentPassword: string }) => IpcResult<AccountSettings>;
|
||||
"settings:email": (p: { email: string; currentPassword: string }) => IpcResult<AccountSettings>;
|
||||
"settings:password": (p: {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}) => IpcResult<AccountSettings>;
|
||||
"settings:privacy": (p: {
|
||||
sharedConnectionsHidden?: boolean;
|
||||
discordFriendsHidden?: boolean;
|
||||
}) => IpcResult<AccountSettings>;
|
||||
"settings:status": (p: { status: UserStatus; statusDescription: string }) => IpcResult<void>;
|
||||
"settings:contentFilters": (filters: ContentFilterKey[]) => IpcResult<AccountSettings>;
|
||||
"settings:enable2fa": () => IpcResult<Pending2Fa>;
|
||||
"settings:verify2fa": (code: string) => IpcResult<{ verified: boolean }>;
|
||||
"settings:disable2fa": () => IpcResult<AccountSettings>;
|
||||
"settings:recoveryCodes": () => IpcResult<RecoveryCode[]>;
|
||||
"settings:reverify2fa": (p: {
|
||||
method: TwoFactorMethod;
|
||||
code: string;
|
||||
}) => IpcResult<{ verified: boolean }>;
|
||||
"settings:resetUserData": () => IpcResult<void>;
|
||||
"settings:deleteAccount": () => IpcResult<AccountSettings>;
|
||||
|
||||
"config:get": () => IpcResult<AppConfig>;
|
||||
"config:setGamePath": (p: { gamePath: string | null }) => IpcResult<AppConfig>;
|
||||
"config:pickGamePath": () => IpcResult<AppConfig>;
|
||||
|
||||
"game:status": () => IpcResult<GameStatus>;
|
||||
"game:launch": () => IpcResult<GameStatus>;
|
||||
|
||||
"gallery:snapshot": () => IpcResult<GallerySnapshot>;
|
||||
"gallery:reveal": (path: string) => IpcResult<void>;
|
||||
"gallery:openExternal": (path: string) => IpcResult<void>;
|
||||
"gallery:delete": (paths: string[]) => IpcResult<void>;
|
||||
"gallery:thumbStats": () => IpcResult<ThumbCacheStats>;
|
||||
"gallery:thumbClear": () => IpcResult<ThumbCacheStats>;
|
||||
|
||||
"enhancements:snapshot": () => IpcResult<EnhancementsSnapshot>;
|
||||
"enhancements:setEnabled": (p: {
|
||||
id: EnhancementId;
|
||||
enabled: boolean;
|
||||
}) => IpcResult<EnhancementsSnapshot>;
|
||||
|
||||
"debug:snapshot": () => IpcResult<DebugSnapshot>;
|
||||
"debug:cacheInvalidate": (key: string) => IpcResult<CacheUpdate>;
|
||||
"debug:cacheClear": () => IpcResult<CacheUpdate>;
|
||||
"debug:repoStats": () => IpcResult<RepoStats[]>;
|
||||
"debug:repoInspect": (name: string) => IpcResult<StoredEntity<{ id: string }>[]>;
|
||||
"debug:repoClear": (name: string) => IpcResult<RepoStats[]>;
|
||||
"debug:repoFlush": (name: string) => IpcResult<RepoStats[]>;
|
||||
"debug:openWindow": () => IpcResult<void>;
|
||||
}
|
||||
|
||||
export interface IpcEvents {
|
||||
"auth:changed": AuthStatus;
|
||||
"accounts:changed": AccountsState;
|
||||
"social:seed": SocialSnapshot;
|
||||
"social:upsert": UserProfile;
|
||||
"world:seed": WorldSnapshot;
|
||||
"world:upsert": World;
|
||||
"world:favoriteFolders": { userId: string; folders: FavoriteWorldFolder[]; done: boolean };
|
||||
"game:changed": GameStatus;
|
||||
"gallery:added": Photo;
|
||||
"debug:log": LogEntry;
|
||||
"debug:cache": CacheUpdate;
|
||||
"ws:event": WsEvent;
|
||||
}
|
||||
|
||||
export type IpcRequestChannel = keyof IpcRequests;
|
||||
export type IpcEventChannel = keyof IpcEvents;
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface AppConfig {
|
||||
version: string;
|
||||
gamePath: string | null;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export type TwoFactorMethod = "totp" | "emailOtp";
|
||||
|
||||
export type AuthStatus =
|
||||
| { state: "unauthenticated" }
|
||||
| { state: "awaiting2fa"; methods: TwoFactorMethod[] }
|
||||
| { state: "authenticated"; user: CurrentUserSummary };
|
||||
|
||||
export interface CurrentUserSummary {
|
||||
id: string;
|
||||
displayName: string;
|
||||
userIcon: string;
|
||||
currentAvatarThumbnailImageUrl: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
displayName: string;
|
||||
userIcon: string;
|
||||
}
|
||||
|
||||
export interface AccountsState {
|
||||
accounts: Account[];
|
||||
activeId: string | null;
|
||||
}
|
||||
|
||||
export interface LoginCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorPayload {
|
||||
method: TwoFactorMethod;
|
||||
code: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface Avatar {
|
||||
id: string;
|
||||
name: string;
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
thumbnailImageUrl: string;
|
||||
releaseStatus: string;
|
||||
tags: string[];
|
||||
favorites: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { RepoStats } from "./repository";
|
||||
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
export interface LogEntry {
|
||||
id: number;
|
||||
ts: number;
|
||||
level: LogLevel;
|
||||
scope: string;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
export type CacheStatus = "fresh" | "stale" | "expired";
|
||||
|
||||
export interface CacheEntryInfo {
|
||||
key: string;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
hardExpiresAt: number;
|
||||
hits: number;
|
||||
lastAccess: number;
|
||||
size: number;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
entries: number;
|
||||
inflight: number;
|
||||
totalSize: number;
|
||||
fresh: number;
|
||||
stale: number;
|
||||
expired: number;
|
||||
hits: number;
|
||||
misses: number;
|
||||
sets: number;
|
||||
patches: number;
|
||||
revalidations: number;
|
||||
invalidations: number;
|
||||
clears: number;
|
||||
persisted: boolean;
|
||||
persistFile: string | null;
|
||||
}
|
||||
|
||||
export interface WsEvent {
|
||||
id: number;
|
||||
ts: number;
|
||||
type: string;
|
||||
handled: boolean;
|
||||
content: unknown;
|
||||
}
|
||||
|
||||
export interface DebugSnapshot {
|
||||
cache: CacheEntryInfo[];
|
||||
stats: CacheStats;
|
||||
logs: LogEntry[];
|
||||
ws: WsEvent[];
|
||||
repos: RepoStats[];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export type EnhancementId = "linux-screenshot-symlink";
|
||||
|
||||
export type OsPlatform = "linux" | "win32" | "darwin";
|
||||
|
||||
export interface EnhancementDetail {
|
||||
key: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface EnhancementState {
|
||||
id: EnhancementId;
|
||||
enabled: boolean;
|
||||
detail?: EnhancementDetail;
|
||||
resolvedPath?: string;
|
||||
}
|
||||
|
||||
export interface EnhancementsSnapshot {
|
||||
platform: OsPlatform;
|
||||
states: EnhancementState[];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface PhotoMetadata {
|
||||
author?: string;
|
||||
authorId?: string;
|
||||
worldId?: string;
|
||||
worldName?: string;
|
||||
takenAt?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface Photo {
|
||||
id: string;
|
||||
fileName: string;
|
||||
bucket: string;
|
||||
src: string;
|
||||
thumb: string;
|
||||
sizeBytes: number;
|
||||
modifiedAt: string;
|
||||
metadata: PhotoMetadata;
|
||||
}
|
||||
|
||||
export interface ThumbCacheStats {
|
||||
count: number;
|
||||
totalBytes: number;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export interface GallerySnapshot {
|
||||
roots: string[];
|
||||
empty: boolean;
|
||||
photos: Photo[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GameStatus {
|
||||
running: boolean;
|
||||
supported: boolean;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
shortCode?: string;
|
||||
description?: string;
|
||||
iconUrl?: string;
|
||||
bannerUrl?: string;
|
||||
ownerId?: string;
|
||||
memberCount?: number;
|
||||
privacy?: string;
|
||||
isRepresenting?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type FieldSource = "ws" | "rest:detail" | "rest:list" | "rest:search" | "seed";
|
||||
|
||||
export interface FieldMeta {
|
||||
at: number;
|
||||
src: FieldSource;
|
||||
}
|
||||
|
||||
export interface EntityMeta {
|
||||
fields: Record<string, FieldMeta>;
|
||||
firstSeen: number;
|
||||
lastRead: number;
|
||||
lastFetch: number;
|
||||
}
|
||||
|
||||
export interface StoredEntity<T> {
|
||||
data: T;
|
||||
meta: EntityMeta;
|
||||
}
|
||||
|
||||
export interface RepoStats {
|
||||
name: string;
|
||||
count: number;
|
||||
totalSize: number;
|
||||
oldestRead: number | null;
|
||||
newestFetch: number | null;
|
||||
pendingWrites: number;
|
||||
backendFile: string | null;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export type ApiErrorCode =
|
||||
| "unauthorized"
|
||||
| "requires_2fa"
|
||||
| "invalid_2fa"
|
||||
| "rate_limited"
|
||||
| "not_found"
|
||||
| "network"
|
||||
| "unknown";
|
||||
|
||||
export interface ApiError {
|
||||
code: ApiErrorCode;
|
||||
message: string;
|
||||
retryAfter?: number;
|
||||
methods?: ("totp" | "emailOtp")[];
|
||||
}
|
||||
|
||||
export type IpcResult<T> = { ok: true; data: T } | { ok: false; error: ApiError };
|
||||
@@ -0,0 +1,46 @@
|
||||
export type ContentFilterKey =
|
||||
| "content_sex"
|
||||
| "content_adult"
|
||||
| "content_violence"
|
||||
| "content_gore"
|
||||
| "content_horror";
|
||||
|
||||
export interface AccountLink {
|
||||
linked: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface AccountSettings {
|
||||
id: string;
|
||||
displayName: string;
|
||||
displayNameChangedAt?: string;
|
||||
previousDisplayName?: string;
|
||||
supporter: boolean;
|
||||
pronouns: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
pendingEmail?: string;
|
||||
twoFactorEnabled: boolean;
|
||||
twoFactorEnabledDate?: string;
|
||||
usesGeneratedPassword: boolean;
|
||||
ageVerificationStatus: "18+" | "hidden" | "verified";
|
||||
ageVerified: boolean;
|
||||
isAdult: boolean;
|
||||
contentFilters: ContentFilterKey[];
|
||||
contentFiltersLocked: boolean;
|
||||
sharedConnectionsHidden: boolean;
|
||||
discordFriendsHidden: boolean;
|
||||
discord: AccountLink;
|
||||
google: AccountLink;
|
||||
accountDeletionDate?: string | null;
|
||||
}
|
||||
|
||||
export interface Pending2Fa {
|
||||
secret: string;
|
||||
qrCodeDataUrl: string;
|
||||
}
|
||||
|
||||
export interface RecoveryCode {
|
||||
code: string;
|
||||
used: boolean;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
export type TrustRank =
|
||||
| "visitor"
|
||||
| "new"
|
||||
| "user"
|
||||
| "known"
|
||||
| "trusted"
|
||||
| "veteran"
|
||||
| "nuisance"
|
||||
| "troll";
|
||||
|
||||
export type UserStatus = "active" | "join me" | "ask me" | "busy" | "offline";
|
||||
|
||||
export type Platform = "standalonewindows" | "android" | "web" | "offline" | string;
|
||||
|
||||
export type Location = "offline" | "private" | "traveling" | "" | string;
|
||||
|
||||
export interface ParsedLocation {
|
||||
worldId: string;
|
||||
instanceId: string;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
export function parseLocation(loc?: string): ParsedLocation | null {
|
||||
if (!loc || loc === "offline" || loc === "private" || loc === "traveling") return null;
|
||||
const [worldId, rest] = loc.split(":");
|
||||
if (!worldId?.startsWith("wrld_") || !rest) return null;
|
||||
const instanceId = rest.split("~")[0];
|
||||
const region = /~region\(([^)]+)\)/.exec(rest)?.[1];
|
||||
return { worldId, instanceId, region };
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
showcased: boolean;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
bio: string;
|
||||
bioLinks: string[];
|
||||
statusDescription: string;
|
||||
status: UserStatus;
|
||||
trustRank: TrustRank;
|
||||
tags: string[];
|
||||
|
||||
userIcon: string;
|
||||
profilePicOverride: string;
|
||||
profilePicOverrideThumbnail: string;
|
||||
currentAvatarImageUrl: string;
|
||||
currentAvatarThumbnailImageUrl: string;
|
||||
currentAvatarTags: string[];
|
||||
|
||||
location?: Location;
|
||||
lastPlatform?: Platform;
|
||||
lastLogin?: string;
|
||||
lastActivity?: string;
|
||||
state?: "online" | "active" | "offline";
|
||||
platform?: Platform;
|
||||
|
||||
isFriend: boolean;
|
||||
friendKey?: string;
|
||||
|
||||
developerType?: "none" | "trusted" | "internal" | "moderator" | string;
|
||||
ageVerificationStatus?: string;
|
||||
ageVerified?: boolean;
|
||||
pronouns?: string;
|
||||
languages?: string[];
|
||||
dateJoined?: string;
|
||||
pastDisplayNames?: { displayName: string; updatedAt?: string }[];
|
||||
note?: string;
|
||||
badges: Badge[];
|
||||
|
||||
isSelf: boolean;
|
||||
}
|
||||
|
||||
export interface SocialSnapshot {
|
||||
selfId: string | null;
|
||||
users: UserProfile[];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export type ReleaseStatus = "public" | "private" | "hidden" | "all";
|
||||
|
||||
export interface WorldPlatforms {
|
||||
pc: boolean;
|
||||
android: boolean;
|
||||
}
|
||||
|
||||
export interface World {
|
||||
id: string;
|
||||
detailed: boolean;
|
||||
name: string;
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
thumbnailImageUrl: string;
|
||||
releaseStatus: ReleaseStatus;
|
||||
capacity: number;
|
||||
favorites: number;
|
||||
visits: number;
|
||||
occupants: number;
|
||||
heat: number;
|
||||
tags: string[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
|
||||
recommendedCapacity?: number;
|
||||
popularity?: number;
|
||||
version?: number;
|
||||
publishedAt?: string;
|
||||
labsPublishedAt?: string;
|
||||
previewYoutubeId?: string;
|
||||
platforms?: WorldPlatforms;
|
||||
publicOccupants?: number;
|
||||
privateOccupants?: number;
|
||||
}
|
||||
|
||||
export interface WorldSnapshot {
|
||||
worlds: World[];
|
||||
byAuthor: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface FavoriteWorldFolder {
|
||||
name: string;
|
||||
displayName: string;
|
||||
worldIds: string[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const TRAFFIC_LIGHT_INSET = 16;
|
||||
const TRAFFIC_LIGHT_CLUSTER_WIDTH = 52;
|
||||
const TRAFFIC_LIGHT_GUTTER = -8;
|
||||
|
||||
export const MAC_CONTENT_INSET =
|
||||
TRAFFIC_LIGHT_INSET + TRAFFIC_LIGHT_CLUSTER_WIDTH + TRAFFIC_LIGHT_GUTTER;
|
||||
Reference in New Issue
Block a user