9 Commits

Author SHA1 Message Date
363f1008c6 update 2026-02-11 07:05:20 +06:00
1bd8aa0fea cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 51s
2026-02-06 23:14:13 +06:00
626f52c616 cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 50s
2026-02-06 23:06:50 +06:00
29914d73a0 cringe sfx 2026-02-06 23:06:42 +06:00
dd530266f9 cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 49s
2026-02-06 22:44:11 +06:00
a37b2048fe cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 53s
2026-02-06 22:41:58 +06:00
e3ac3e003c cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 2m35s
2026-02-06 22:32:01 +06:00
6fa142f133 productName typo 2026-02-03 22:06:09 +06:00
8e0a08da05 icons
All checks were successful
Deploy / publish-web (push) Successful in 44s
2026-02-03 22:02:45 +06:00
226 changed files with 534 additions and 66 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(printf '%s\\\\n' \"\n================================================================================\nCOMPREHENSIVE ARCHITECTURE REPORT\nTauri 2 + Nuxt 4 + mediasoup-client Voice Chat App\n================================================================================\")"
]
}
}

Binary file not shown.

118
client/CLAUDE.md Normal file
View File

@@ -0,0 +1,118 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Chad is a voice/video chat desktop app built with **Tauri 2 + Nuxt 4 (SPA) + mediasoup-client**. It uses an SFU (Selective Forwarding Unit) architecture for WebRTC media — each peer sends to the server, which forwards selectively to others.
## Commands
| Command | Description |
|---------|-------------|
| `yarn dev` | Start dev server (binds to all interfaces via `--host`) |
| `yarn generate` | Generate static site to `.output/public` |
| `yarn build` | Build for production |
| `yarn preview` | Preview production build |
| `npx eslint .` | Lint the project |
| `npx eslint --fix .` | Lint and auto-fix |
**Tauri desktop build**: `yarn tauri build` (runs `yarn generate` first automatically).
Package manager is **Yarn 4.12.0**. No test framework is configured.
## Architecture
### Composable-Based State (No Pinia/Vuex)
All state is managed through Vue composables using `@vueuse/core`'s `createGlobalState` and `createSharedComposable`. Nuxt auto-imports all composables from `app/composables/`.
**Dependency graph** (arrows = "depends on"):
```
useAuth (foundation — user session)
└→ useSignaling (Socket.IO connection to /webrtc namespace)
└→ useClients (connected peer list)
└→ useMediasoup (transports, producers, consumers)
├← usePreferences (device IDs, audio effects, hotkeys)
│ └← useDevices (enumerates hardware via getUserMedia)
└← useSfx (Howler.js sound effects)
useApp (top-level orchestrator — mute/unmute, video/share toggles)
└→ depends on useClients, useMediasoup, useSignaling, useSfx
```
### Composable Tiers
**Global state** (`createGlobalState` — single instance, persists for app lifetime):
- `useApp` — ready state, input/output mute, video/share toggles, version info
- `useAuth``me` ref, login/register/logout
- `useClients``clients[]` array, add/remove/update peers
- `usePreferences` — device selections, audio effects, hotkeys (localStorage + server sync)
- `useUpdater` — Tauri app update checks
- `useFullscreenVideo` — fullscreen video element control
**Shared** (`createSharedComposable` — single instance, disposed when last consumer unmounts):
- `useMediasoup` — mediasoup Device, transports, producers, consumers, speaking state
- `useSignaling` — Socket.IO client, connect/disconnect
- `useSfx` — sound effect playback via Howler.js
**Per-instance** (new instance per call):
- `useClient(socketId)` — per-peer volume/mute (localStorage), filtered consumers/producers
- `useAudioContext(audioTrack)` — Web Audio API gain node for per-client volume
- `useDevices` — media device enumeration
### Initialization Flow
1. **Middleware** (global, ordered by filename prefix):
- `00.updater` — Tauri update check, redirects to `/updater` if available
- `01.auth` — validates session via `GET /me`, redirects guests to `/login`
- `02.user-preferences` — loads server-synced preferences for authenticated users
2. **Plugins**: build info logging, Tauri hotkey registration
3. **Layout** (`default.vue`): calls `useApp()` which triggers `useSignaling().connect()`
4. **Socket authenticated** → mediasoup Device created → transports created → `join()` → mic enabled
### mediasoup Integration
**Producer types** (tracked in `appData.source`):
- Microphone: `kind='audio'`, `source='mic-video'`
- Camera: `kind='video'`, `source='mic-video'`
- Screen share: `kind='video'`, `source='share'`
**Consumer filtering** uses `kind` + `appData.source` to distinguish audio/video/share streams.
Transports use `emitWithAck()` on the Socket.IO connection for all signaling (create, connect, produce, join, pause/resume/close).
### API & Networking
- **REST API**: `shared/chad-api.ts``$fetch` instance with `credentials: 'include'`, base URL from `__API_BASE_URL__` build-time define
- **WebSocket**: Socket.IO to `/webrtc` namespace for signaling
- **Vite proxy**: `/api` → production API server (for dev)
### Shared Types
`shared/types.ts` defines `ChadClient`, `Consumer`, `Producer`, `AppData`, `UpdatedClient` — used by both composables and components.
## Code Conventions
- **ESLint**: `@antfu/eslint-config` with formatters enabled
- **Vue SFC block order**: `<template>`, `<script>`, `<style>` (enforced by ESLint)
- **`console.log` allowed** (no-console is off)
- **PrimeVue components** are prefixed: `PrimeButton`, `PrimeCard`, etc.
- **Icons**: `lucide-vue-next` + `primeicons`
- **Styling**: Tailwind CSS 4 + PrimeVue Aura theme (Zinc palette, dark-first)
- **Reactivity**: mediasoup objects wrapped with `markRaw()` to prevent deep reactivity; `triggerRef()` used for manual reactivity triggers on the clients array
## Build-Time Defines
Set via `.env` or environment variables:
- `API_BASE_URL` — backend API base (default: `http://localhost:4000/chad`)
- `COMMIT_SHA` — git commit hash (default: `'local'`)
## Tauri
- App identifier: `xyz.koptilnya.chad`
- Windows-only NSIS installer (`bundle.targets: ["nsis"]`)
- Plugins: global-shortcut, process, updater, single-instance, log
- Frontend dist: `.output/public` (from `yarn generate`)
- `@tauri-apps/plugin-*` packages provide JS bindings; use `window.__TAURI__` check or the `useTauri()` composable for Tauri detection

View File

@@ -10,17 +10,18 @@
class="absolute bottom-2 text-sm opacity-70 group-hover:opacity-100"
rounded
>
{{ isMe ? 'You' : client.username }}
{{ isMe ? 'You' : client.displayName }}
</PrimeTag>
</div>
</template>
<script setup lang="ts">
import type { ChadClient } from '#shared/types'
import type { ChadClient, Consumer, Producer } from '#shared/types'
const props = defineProps<{
client: ChadClient
stream: MediaStream
consumer?: Consumer
producer?: Producer
}>()
const { me } = useClients()
@@ -30,8 +31,21 @@ const isMe = computed(() => {
return props.client.socketId === me.value?.socketId
})
const track = computed(() => {
return props.consumer?.raw.track ?? props.producer?.raw.track
})
const stream = computed<MediaStream | undefined>((previousStream) => {
if (previousStream?.getTracks()[0] === track.value) {
return previousStream
}
return track.value ? new MediaStream([track.value]) : undefined
})
function watch() {
fullscreenVideo.show(props.stream)
if (stream.value)
fullscreenVideo.show(stream.value)
}
</script>

View File

@@ -7,7 +7,7 @@ export const useApp = createGlobalState(() => {
const { clients } = useClients()
const mediasoup = useMediasoup()
const signaling = useSignaling()
const toast = useToast()
const { emit } = useEventBus()
const ready = ref(false)
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
@@ -52,7 +52,7 @@ export const useApp = createGlobalState(() => {
await mediasoup.pauseProducer(mediasoup.micProducer.value)
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
emit('audio:muted')
}
async function unmuteInput() {
@@ -65,7 +65,7 @@ export const useApp = createGlobalState(() => {
await mediasoup.resumeProducer(mediasoup.micProducer.value)
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
emit('audio:unmuted')
}
async function toggleInput() {
@@ -89,7 +89,7 @@ export const useApp = createGlobalState(() => {
outputMuted: true,
})
toast.add({ severity: 'info', summary: 'Sound muted', closable: false, life: 1000 })
emit('output:muted')
}
async function unmuteOutput() {
@@ -102,7 +102,7 @@ export const useApp = createGlobalState(() => {
outputMuted: false,
})
toast.add({ severity: 'info', summary: 'Sound resumed', closable: false, life: 1000 })
emit('output:unmuted')
}
async function toggleOutput() {
@@ -115,18 +115,22 @@ export const useApp = createGlobalState(() => {
async function toggleVideo() {
if (!mediasoup.videoProducer.value) {
await mediasoup.enableVideo()
emit('video:enabled')
}
else {
await mediasoup.disableProducer(mediasoup.videoProducer.value)
emit('video:disabled')
}
}
async function toggleShare() {
if (!mediasoup.shareProducer.value) {
await mediasoup.enableShare()
emit('share:enabled')
}
else {
await mediasoup.disableProducer(mediasoup.shareProducer.value)
emit('share:disabled')
}
}

View File

@@ -4,7 +4,7 @@ import { createGlobalState } from '@vueuse/core'
export const useClients = createGlobalState(() => {
const auth = useAuth()
const signaling = useSignaling()
const toast = useToast()
const { emit } = useEventBus()
const clients = shallowRef<ChadClient[]>([])
@@ -16,10 +16,17 @@ export const useClients = createGlobalState(() => {
socket.on('clientChanged', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
const client = getClient(clientId)
if (!client)
return
updateClient(clientId, updatedClient)
if (client && client.displayName !== updatedClient.displayName)
toast.add({ severity: 'info', summary: `${client.displayName} is now ${updatedClient.displayName}`, closable: false, life: 1000 })
emit('client:updated', {
socketId: clientId,
oldClient: client,
updatedClient,
})
})
socket.on('disconnect', () => {

View File

@@ -0,0 +1,42 @@
import type { ChadClient, Consumer, Producer } from '#shared/types'
import type { EventType } from 'mitt'
import mitt from 'mitt'
export interface AppEvents extends Record<EventType, unknown> {
'socket:connected': void
'socket:disconnected': void
'socket:authenticated': { socketId: string }
'client:added': ChadClient
'client:removed': ChadClient
'client:updated': { socketId: string, oldClient: ChadClient, updatedClient: Partial<ChadClient> }
'consumer:added': Consumer
'consumer:removed': Consumer
'consumer:paused': Consumer
'consumer:resumed': Consumer
'producer:added': Producer
'producer:removed': Producer
'producer:paused': Producer
'producer:resumed': Producer
'audio:muted': void
'audio:unmuted': void
'output:muted': void
'output:unmuted': void
'video:enabled': void
'video:disabled': void
'share:enabled': void
'share:disabled': void
}
const emitter = mitt<AppEvents>()
export function useEventBus() {
return {
emit: emitter.emit,
on: emitter.on,
off: emitter.off,
}
}

View File

@@ -7,8 +7,6 @@ import { useDevices } from '~/composables/use-devices'
import { usePreferences } from '~/composables/use-preferences'
import { useSignaling } from '~/composables/use-signaling'
type ProducerType = 'microphone' | 'video' | 'share'
interface SpeakingClient {
clientId: ChadClient['socketId']
volume: number
@@ -28,10 +26,10 @@ const ICE_SERVERS: RTCIceServer[] = [
]
export const useMediasoup = createSharedComposable(() => {
const toast = useToast()
const { emit } = useEventBus()
const signaling = useSignaling()
const { addClient, removeClient } = useClients()
const { addClient, removeClient, me } = useClients()
const preferences = usePreferences()
const { getShareStream } = useDevices()
@@ -169,17 +167,25 @@ export const useMediasoup = createSharedComposable(() => {
addClient(...joinedClients)
toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 })
if (me.value)
emit('socket:authenticated', { socketId: me.value.socketId })
await enableMic()
})
socket.on('newPeer', (client) => {
addClient(client)
emit('client:added', client)
})
socket.on('peerClosed', (id) => {
const { getClient } = useClients()
const client = getClient(id)
removeClient(id)
if (client)
emit('client:removed', client)
})
socket.on(
@@ -210,16 +216,27 @@ export const useMediasoup = createSharedComposable(() => {
raw: markRaw(consumer),
}
emit('consumer:added', consumers.value[consumer.id]!)
consumer.observer.on('resume', () => {
consumers.value[consumer.id]!.paused = false
emit('consumer:resumed', consumers.value[consumer.id]!)
})
consumer.observer.on('pause', () => {
consumers.value[consumer.id]!.paused = true
emit('consumer:paused', consumers.value[consumer.id]!)
})
consumer.observer.on('close', () => {
const consumerData = consumers.value[consumer.id]
delete consumers.value[consumer.id]
if (consumerData)
emit('consumer:removed', consumerData)
})
consumer.on('trackended', () => {
@@ -300,16 +317,27 @@ export const useMediasoup = createSharedComposable(() => {
raw: markRaw(producer),
}
emit('producer:added', producers.value[producer.id]!)
producer.observer.on('pause', () => {
producers.value[producer.id]!.paused = true
emit('producer:paused', producers.value[producer.id]!)
})
producer.observer.on('resume', () => {
producers.value[producer.id]!.paused = false
emit('producer:resumed', producers.value[producer.id]!)
})
producer.observer.on('close', () => {
const producerData = producers.value[producer.id]
delete producers.value[producer.id]
if (producerData)
emit('producer:removed', producerData)
})
producer.on('trackended', () => {

View File

@@ -0,0 +1,93 @@
import { createSharedComposable } from '@vueuse/core'
import { Howl } from 'howler'
const CONNECTION_SOUNDS = Object.keys(import.meta.glob('@/../public/sfx/connection/*.ogg')).map(path => path.replace('../public', ''))
console.log('CONNECTION_SOUNDS', CONNECTION_SOUNDS)
function hashStringToNumber(str: string, cap: number): number {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) | 0
}
return Math.abs(hash) % cap
}
const oneShots: Howl[] = []
type SfxEvent = 'mic-on' | 'mic-off' | 'stream-on' | 'stream-off' | 'connection'
const EVENT_VOLUME: Record<SfxEvent, number> = {
'mic-on': 0.2,
'mic-off': 0.2,
'stream-on': 0.03,
'stream-off': 0.03,
'connection': 0.1,
}
// TODO: refactor this shit
export const useSfx = createSharedComposable(() => {
const { outputMuted } = useApp()
async function play(src: string, volume = 0.2): Promise<void> {
return new Promise((resolve) => {
const howl = new Howl({
src,
autoplay: true,
loop: false,
volume,
})
howl.on('end', () => {
resolve()
})
})
}
async function playOneShot(src: string, volume = 0.2): Promise<void> {
for (const oneShot of oneShots) {
oneShot.stop()
}
oneShots.length = 0
return new Promise((resolve) => {
const howl = new Howl({
src,
autoplay: true,
loop: false,
volume,
})
oneShots.push(howl)
howl.on('end', () => {
resolve()
})
})
}
async function playEvent(event: SfxEvent) {
switch (event) {
default:
await playOneShot(`/sfx/${event}.ogg`, EVENT_VOLUME[event])
break
}
}
async function playRandomConnectionSound(seed: string) {
await playEvent('stream-on')
if (outputMuted.value)
return
await play(CONNECTION_SOUNDS[hashStringToNumber(seed, CONNECTION_SOUNDS.length)]!, 0.1)
}
return {
playOneShot,
play,
playRandomConnectionSound,
playEvent,
}
})

View File

@@ -4,7 +4,7 @@ import { io } from 'socket.io-client'
import { parseURL } from 'ufo'
export const useSignaling = createSharedComposable(() => {
const toast = useToast()
const { emit } = useEventBus()
const { me } = useAuth()
const socket = shallowRef<Socket>()
@@ -41,9 +41,9 @@ export const useSignaling = createSharedComposable(() => {
watch(connected, (connected) => {
if (connected)
toast.add({ severity: 'success', summary: 'Connected', closable: false, life: 1000 })
emit('socket:connected')
else
toast.add({ severity: 'error', summary: 'Disconnected', closable: false, life: 1000 })
emit('socket:disconnected')
}, { immediate: true })
watch(me, (me) => {

View File

@@ -40,7 +40,7 @@
<PrimeSelectButton
v-model="activeTab"
:options="tabs"
data-key="id"
option-label="id"
:allow-empty="false"
style="--p-togglebutton-content-padding: 0.25rem 0.5rem"
>

View File

@@ -1,21 +1,23 @@
<template>
<div class="grid grid-cols-[1fr_1fr] gap-2">
<GalleryCard
v-for="item in gallery"
:key="item.client.socketId"
:client="item.client"
:stream="item.stream"
v-for="producer in producers"
:key="`producer-${producer.id}`"
:client="me!"
:producer="producer"
/>
<GalleryCard
v-for="consumer in consumers"
:key="`consumer-${consumer.consumer.id}`"
:client="consumer.client"
:consumer="consumer.consumer"
/>
</div>
</template>
<script setup lang="ts">
import type { ChadClient } from '#shared/types'
interface GalleryItem {
client: ChadClient
stream: MediaStream
}
import type { ChadClient, Consumer, Producer } from '#shared/types'
definePageMeta({
name: 'Gallery',
@@ -24,39 +26,29 @@ definePageMeta({
const { videoProducer, shareProducer } = useMediasoup()
const { clients, me } = useClients()
const gallery = computed(() => {
return clients.value.reduce<GalleryItem[]>(
(acc, client) => {
const { streaming, videoConsumers, shareConsumers } = useClient(client.socketId)
const producers = computed(() => {
return [videoProducer.value, shareProducer.value].filter(p => !!p && !!p.raw.track) as Producer[]
})
const consumers = computed(() => {
return clients.value.reduce<{ client: ChadClient, consumer: Consumer }[]>((acc, client) => {
const { streaming, videoConsumers: clientVideoConsumers, shareConsumers: clientShareConsumers } = useClient(client.socketId)
if (!streaming.value)
return acc
for (const consumer of [...videoConsumers.value, ...shareConsumers.value]) {
acc.push({
client,
stream: new MediaStream([consumer.raw.track]),
})
for (const consumer of [...clientVideoConsumers.value, ...clientShareConsumers.value]) {
acc.push({ client, consumer })
}
return acc
},
[videoProducer.value, shareProducer.value].reduce<GalleryItem[]>((acc, producer) => {
if (!me.value || !producer || !producer.raw.track)
return acc
acc.push({
client: me.value,
stream: new MediaStream([producer.raw.track]),
}, [])
})
return acc
}, []),
)
})
const hasItems = computed(() => producers.value.length > 0 || consumers.value.length > 0)
watch(gallery, (gallery) => {
if (gallery.length > 0)
watch(hasItems, (hasItems) => {
if (hasItems)
return
navigateTo({ name: 'Index' })

View File

@@ -74,7 +74,14 @@
<p class="text-sm mb-2 text-center">
FPS
</p>
<PrimeSelectButton v-model="shareFps" :options="[5, 30, 60]" fluid size="small" />
<PrimeSelectButton
v-model="shareFps"
:options="shareFpsOptions"
fluid
size="small"
option-label="label"
option-value="value"
/>
</div>
<template v-if="isTauri">
@@ -152,6 +159,13 @@ const {
shareFps,
} = usePreferences()
const shareFpsOptions = [5, 30, 60].map((value) => {
return {
label: value.toString(),
value,
}
})
const setupToggleInputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleInputHotkey)
const setupToggleOutputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleOutputHotkey)

View File

@@ -0,0 +1,57 @@
export default defineNuxtPlugin(() => {
const { on } = useEventBus()
const sfx = useSfx()
// Connection sounds
on('socket:authenticated', ({ socketId }) => {
sfx.playRandomConnectionSound(socketId)
})
// Client events
on('client:added', (client) => {
sfx.playRandomConnectionSound(client.socketId)
})
on('client:removed', () => {
sfx.playEvent('stream-off')
})
// Audio mute/unmute
on('audio:muted', () => {
sfx.playEvent('mic-off')
})
on('audio:unmuted', () => {
sfx.playEvent('mic-on')
})
// Video/share toggle
on('video:enabled', () => {
sfx.playEvent('stream-on')
})
on('video:disabled', () => {
sfx.playEvent('stream-off')
})
on('share:enabled', () => {
sfx.playEvent('stream-on')
})
on('share:disabled', () => {
sfx.playEvent('stream-off')
})
// Consumer video streams
on('consumer:added', (consumer) => {
if (consumer.raw.kind === 'video') {
sfx.playEvent('stream-on')
}
})
on('consumer:removed', (consumer) => {
if (consumer.raw.kind === 'video') {
sfx.playEvent('stream-off')
}
})
})

View File

@@ -0,0 +1,62 @@
export default defineNuxtPlugin(() => {
const { on } = useEventBus()
const toast = useToast()
// Socket events
on('socket:connected', () => {
toast.add({ severity: 'success', summary: 'Connected', life: 1000 })
})
on('socket:disconnected', () => {
toast.add({ severity: 'error', summary: 'Disconnected', life: 1000 })
})
on('socket:authenticated', () => {
toast.add({ severity: 'success', summary: 'Joined', life: 1000 })
})
// Client events
on('client:added', (client) => {
toast.add({
severity: 'info',
summary: `${client.displayName} connected`,
life: 1000,
})
})
on('client:removed', (client) => {
toast.add({
severity: 'info',
summary: `${client.displayName} disconnected`,
life: 1000,
})
})
on('client:updated', ({ oldClient, updatedClient }) => {
if (oldClient.displayName !== updatedClient.displayName) {
toast.add({
severity: 'info',
summary: `${oldClient.displayName} is now ${updatedClient.displayName}`,
life: 1000,
})
}
})
// Audio control
on('audio:muted', () => {
toast.add({ severity: 'info', summary: 'Microphone muted', life: 1000 })
})
on('audio:unmuted', () => {
toast.add({ severity: 'info', summary: 'Microphone activated', life: 1000 })
})
// Output mute control
on('output:muted', () => {
toast.add({ severity: 'info', summary: 'Sound muted', life: 1000 })
})
on('output:unmuted', () => {
toast.add({ severity: 'info', summary: 'Sound resumed', life: 1000 })
})
})

View File

View File

@@ -19,8 +19,10 @@
"@tauri-apps/plugin-updater": "~2",
"@vueuse/core": "^13.9.0",
"hotkeys-js": "^4.0.0",
"howler": "^2.2.4",
"lucide-vue-next": "^0.562.0",
"mediasoup-client": "^3.18.6",
"mitt": "^3.0.1",
"nuxt": "^4.2.2",
"postcss": "^8.5.6",
"primeicons": "^7.0.0",
@@ -37,6 +39,7 @@
"@antfu/eslint-config": "^5.4.1",
"@primevue/nuxt-module": "^4.4.0",
"@tauri-apps/cli": "^2.8.4",
"@types/howler": "^2",
"eslint": "^9.36.0",
"eslint-plugin-format": "^1.0.2",
"sass-embedded": "^1.93.2",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More