работаем бля работаем

This commit is contained in:
2026-05-09 03:21:44 +06:00
parent f845777bac
commit 0b148c6a7d
169 changed files with 15816 additions and 1005 deletions

Binary file not shown.

54
client/.zed/settings.json Normal file
View File

@@ -0,0 +1,54 @@
{
// Use ESLint's --fix:
"code_actions_on_format": {
"source.fixAll.eslint": true,
},
"formatter": [],
// Enable eslint for all supported languages
// Defaults only include https://github.com/search?q=repo%3Azed-industries%2Fzed%20eslint_languages&type=code
"languages": {
"HTML": {
"language_servers": ["...", "eslint"],
},
"Markdown": {
"language_servers": ["...", "eslint"],
},
"Markdown-Inline": {
"language_servers": ["...", "eslint"],
},
"JSON": {
"language_servers": ["...", "eslint"],
},
"JSONC": {
"language_servers": ["...", "eslint"],
},
"YAML": {
"language_servers": ["...", "eslint"],
},
"CSS": {
"language_servers": ["...", "eslint"],
},
// Add other languages as needed
},
"lsp": {
"eslint": {
"settings": {
"workingDirectories": ["./"],
// Silent the stylistic rules in your IDE, but still auto fix them
"rulesCustomizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true },
],
},
},
},
}

View File

@@ -21,7 +21,7 @@
</PrimeAvatar>
<p class="flex-1 text-sm leading-5 font-medium text-color truncate w-0">
{{ client.displayName || client.username }}
{{ client.displayName || client.username || client.socketId }}
</p>
<Component :is="expanded ? ChevronUp : ChevronDown" v-if="!isMe" :size="20" class="text-muted-color" />

View File

@@ -85,7 +85,7 @@ export const useApp = createGlobalState(() => {
await muteInput()
await signaling.socket.value?.emitWithAck('updateClient', {
await signaling.socket.value?.emitWithAck('update-client', {
outputMuted: true,
})
@@ -98,7 +98,7 @@ export const useApp = createGlobalState(() => {
if (!previousInputMuted.value)
await unmuteInput()
await signaling.socket.value?.emitWithAck('updateClient', {
await signaling.socket.value?.emitWithAck('update-client', {
outputMuted: false,
})

View File

@@ -14,7 +14,7 @@ export const useClients = createGlobalState(() => {
if (!socket)
return
socket.on('clientChanged', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
socket.on('client-updated', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
const client = getClient(clientId)
if (!client)

View File

@@ -26,15 +26,15 @@ const ICE_SERVERS: RTCIceServer[] = [
]
export const useMediasoup = createSharedComposable(() => {
const { emit } = useEventBus()
const eventBus = useEventBus()
const signaling = useSignaling()
const { addClient, removeClient, me } = useClients()
const { addClient, removeClient, me, clients, updateClient } = useClients()
const preferences = usePreferences()
const { getShareStream } = useDevices()
const device = shallowRef<mediasoupClient.Device>()
const rtpCapabilities = shallowRef<mediasoupClient.types.RtpCapabilities>()
const routerRtpCapabilities = shallowRef<mediasoupClient.types.RtpCapabilities>()
const sendTransport = shallowRef<mediasoupClient.types.Transport>()
const recvTransport = shallowRef<mediasoupClient.types.Transport>()
@@ -79,18 +79,30 @@ export const useMediasoup = createSharedComposable(() => {
if (!socket)
return
socket.on('authenticated', async () => {
socket.on('new-client', (client) => {
addClient(client)
eventBus.emit('client:added', client)
})
socket.on('client-switched-channel', (client) => {
updateClient(client.socketId, client)
})
socket.on('initialized', async (initData) => {
if (!signaling.socket.value)
return
device.value = new mediasoupClient.Device()
rtpCapabilities.value = await signaling.socket.value.emitWithAck('getRtpCapabilities')
routerRtpCapabilities.value = initData.rtpCapabilities
await device.value.load({ routerRtpCapabilities: rtpCapabilities.value! })
clients.value = initData.clients
await device.value.load({ routerRtpCapabilities: routerRtpCapabilities.value! })
// Send transport
{
const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: true, consuming: false })
const transportInfo = await signaling.socket.value.emitWithAck('create-transport', { producing: true, consuming: false })
sendTransport.value = device.value.createSendTransport({
...transportInfo,
iceServers: [
@@ -101,7 +113,7 @@ export const useMediasoup = createSharedComposable(() => {
sendTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await signaling.socket.value!.emitWithAck('connectTransport', {
await signaling.socket.value!.emitWithAck('connect-transport', {
transportId: sendTransport.value!.id,
dtlsParameters,
})
@@ -135,7 +147,7 @@ export const useMediasoup = createSharedComposable(() => {
// Recv Transport
{
const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: false, consuming: true })
const transportInfo = await signaling.socket.value.emitWithAck('create-transport', { producing: false, consuming: true })
recvTransport.value = device.value.createRecvTransport({
...transportInfo,
iceServers: [
@@ -146,7 +158,7 @@ export const useMediasoup = createSharedComposable(() => {
recvTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await signaling.socket.value!.emitWithAck('connectTransport', {
await signaling.socket.value!.emitWithAck('connect-transport', {
transportId: recvTransport.value!.id,
dtlsParameters,
})
@@ -160,36 +172,33 @@ export const useMediasoup = createSharedComposable(() => {
}
})
}
//
// const joinedClients = (await signaling.socket.value.emitWithAck('join', {
// rtpCapabilities: routerRtpCapabilities.value,
// }))
//
// addClient(...joinedClients)
//
// if (me.value)
// eventBus.emit('socket:authenticated', { socketId: me.value.socketId })
//
const joinedClients = (await signaling.socket.value.emitWithAck('join', {
rtpCapabilities: rtpCapabilities.value,
}))
addClient(...joinedClients)
if (me.value)
emit('socket:authenticated', { socketId: me.value.socketId })
// TODO: при переподключении проверять inputMuted
await enableMic()
})
socket.on('newPeer', (client) => {
addClient(client)
emit('client:added', client)
})
socket.on('peerClosed', (id) => {
socket.on('client-disconnected', (id) => {
const { getClient } = useClients()
const client = getClient(id)
removeClient(id)
if (client)
emit('client:removed', client)
eventBus.emit('client:removed', client)
})
socket.on(
'newConsumer',
'new-consumer',
async (
{ id, producerId, kind, rtpParameters, socketId, appData, producerPaused },
cb,
@@ -216,18 +225,18 @@ export const useMediasoup = createSharedComposable(() => {
raw: markRaw(consumer),
}
emit('consumer:added', consumers.value[consumer.id]!)
eventBus.emit('consumer:added', consumers.value[consumer.id]!)
consumer.observer.on('resume', () => {
consumers.value[consumer.id]!.paused = false
emit('consumer:resumed', consumers.value[consumer.id]!)
eventBus.emit('consumer:resumed', consumers.value[consumer.id]!)
})
consumer.observer.on('pause', () => {
consumers.value[consumer.id]!.paused = true
emit('consumer:paused', consumers.value[consumer.id]!)
eventBus.emit('consumer:paused', consumers.value[consumer.id]!)
})
consumer.observer.on('close', () => {
@@ -236,7 +245,7 @@ export const useMediasoup = createSharedComposable(() => {
delete consumers.value[consumer.id]
if (consumerData)
emit('consumer:removed', consumerData)
eventBus.emit('consumer:removed', consumerData)
})
consumer.on('trackended', () => {
@@ -248,7 +257,7 @@ export const useMediasoup = createSharedComposable(() => {
)
socket.on(
'consumerClosed',
'consumer-closed',
async (
{ consumerId },
) => {
@@ -261,7 +270,7 @@ export const useMediasoup = createSharedComposable(() => {
},
)
socket.on('consumerPaused', ({ consumerId }) => {
socket.on('consumer-paused', ({ consumerId }) => {
const consumer = consumers.value[consumerId]
if (!consumer)
@@ -270,7 +279,7 @@ export const useMediasoup = createSharedComposable(() => {
consumer.raw.pause()
})
socket.on('consumerResumed', ({ consumerId }) => {
socket.on('consumer-resumed', ({ consumerId }) => {
const consumer = consumers.value[consumerId]
if (!consumer)
@@ -279,13 +288,13 @@ export const useMediasoup = createSharedComposable(() => {
consumer.raw.resume()
})
socket.on('speakingPeers', (value: SpeakingClient[]) => {
socket.on('speaking-clients', (value: SpeakingClient[]) => {
speakingClients.value = value
})
socket.on('disconnect', () => {
device.value = undefined
rtpCapabilities.value = undefined
routerRtpCapabilities.value = undefined
sendTransport.value?.close()
sendTransport.value = undefined
@@ -317,27 +326,28 @@ export const useMediasoup = createSharedComposable(() => {
raw: markRaw(producer),
}
emit('producer:added', producers.value[producer.id]!)
eventBus.emit('producer:added', producers.value[producer.id]!)
producer.observer.on('pause', () => {
producers.value[producer.id]!.paused = true
emit('producer:paused', producers.value[producer.id]!)
eventBus.emit('producer:paused', producers.value[producer.id]!)
})
producer.observer.on('resume', () => {
producers.value[producer.id]!.paused = false
emit('producer:resumed', producers.value[producer.id]!)
eventBus.emit('producer:resumed', producers.value[producer.id]!)
})
producer.observer.on('close', () => {
console.log('producer closed')
const producerData = producers.value[producer.id]
delete producers.value[producer.id]
if (producerData)
emit('producer:removed', producerData)
eventBus.emit('producer:removed', producerData)
})
producer.on('trackended', () => {
@@ -352,7 +362,7 @@ export const useMediasoup = createSharedComposable(() => {
try {
producer.raw.close()
await signaling.socket.value.emitWithAck('closeProducer', {
await signaling.socket.value.emitWithAck('close-producer', {
producerId: producer.id,
})
}
@@ -455,7 +465,7 @@ export const useMediasoup = createSharedComposable(() => {
await createProducer({
track,
streamId: 'share',
codec: device.value.rtpCapabilities.codecs?.find(
codec: device.value.sendRtpCapabilities.codecs?.find(
c => c.mimeType.toLowerCase() === 'video/AV1',
),
codecOptions: {
@@ -478,7 +488,7 @@ export const useMediasoup = createSharedComposable(() => {
try {
producer.raw.pause()
await signaling.socket.value.emitWithAck('pauseProducer', {
await signaling.socket.value.emitWithAck('pause-producer', {
producerId: producer.id,
})
}
@@ -494,7 +504,7 @@ export const useMediasoup = createSharedComposable(() => {
try {
producer.raw.resume()
await signaling.socket.value.emitWithAck('resumeProducer', {
await signaling.socket.value.emitWithAck('resume-producer', {
producerId: producer.id,
})
}
@@ -526,7 +536,7 @@ export const useMediasoup = createSharedComposable(() => {
speakingClients,
sendTransport,
recvTransport,
rtpCapabilities,
rtpCapabilities: routerRtpCapabilities,
device,
micProducer,
videoProducer,
@@ -536,5 +546,7 @@ export const useMediasoup = createSharedComposable(() => {
enableVideo,
enableShare,
disableProducer,
consumersArray,
producersArray,
}
})

View File

@@ -52,11 +52,39 @@
<PrimeScrollPanel class="bg-surface-900 rounded-xl overflow-hidden" style="min-height: 0">
<div v-auto-animate class="p-3 space-y-1">
<ClientRow v-for="client of clients" :key="client.userId" :client="client" />
<template v-for="channel in channels" :key="channel.id">
<PrimeDivider>
<PrimeButton size="small" variant="text" @click="joinChannel(channel)">
{{ channel.name }}
</PrimeButton>
</PrimeDivider>
<ClientRow v-for="client in clients.filter(_client => _client.channelId === channel.id)" :key="client.socketId" :client="client">
{{ client.userId }}
</ClientRow>
</template>
<!-- <ClientRow v-for="client of clients" :key="client.userId" :client="client" /> -->
</div>
</PrimeScrollPanel>
<div class="bg-surface-900 rounded-xl overflow-hidden p-3 flex flex-col min-h-full">
<dl>
<dt>Socket ID</dt>
<dd>{{ socket?.id }}</dd>
<br>
<dt>Producers</dt>
<dd v-for="producer in producersArray" :key="producer.id">
{{ producer.id }}
{{ producer.appData }}
</dd>
<br>
<dl>Consumers</dl>
<dd v-for="consumer in consumersArray" :key="consumer.id">
{{ consumer.id }}
{{ consumer.appData }}
</dd>
</dl>
<slot />
</div>
</div>
@@ -65,6 +93,7 @@
</template>
<script setup lang="ts">
import chadApi from '#shared/chad-api'
import {
Camera,
CameraOff,
@@ -80,6 +109,13 @@ import {
VolumeOff,
} from 'lucide-vue-next'
const channels = shallowRef<any[]>([])
;(async () => {
channels.value = await chadApi<any[]>('/channels', { method: 'GET' })
})()
const { me } = useClients()
const {
version,
clients,
@@ -93,7 +129,8 @@ const {
toggleVideo,
toggleShare,
} = useApp()
const { connect, connected } = useSignaling()
const { connect, connected, socket } = useSignaling()
const { consumersArray, producersArray } = useMediasoup()
interface Tab {
id: string
@@ -150,4 +187,30 @@ watch(activeTab, (activeTab) => {
})
connect()
async function joinChannel(channel) {
socket.value?.emit('join-channel', { channelId: channel.id })
}
watch(socket, (socket) => {
if (!socket)
return
socket.on('channel-removed', (channelId) => {
const idx = channels.value.findIndex(channel => channel.id === channelId)
if (idx === -1)
return
channels.value.splice(idx, 1)
triggerRef(channels)
})
socket.on('channel-created', (channel) => {
channels.value.push(channel)
triggerRef(channels)
})
}, { immediate: true })
</script>

View File

@@ -25,7 +25,7 @@
"linkify-string": "^4.3.2",
"linkifyjs": "^4.3.2",
"lucide-vue-next": "^0.562.0",
"mediasoup-client": "^3.18.6",
"mediasoup-client": "^3.19.0",
"mitt": "^3.0.1",
"nuxt": "^4.2.2",
"postcss": "^8.5.6",

View File

@@ -3,8 +3,7 @@ import type { Consumer as MediasoupConsumer, Producer as MediasoupProducer } fro
export interface ChadClient {
socketId: string
userId: string
username: string
displayName: string
channelId: string
inputMuted?: boolean
outputMuted?: boolean

View File

@@ -3004,7 +3004,7 @@ __metadata:
languageName: node
linkType: hard
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12":
"@types/debug@npm:^4.0.0":
version: 4.1.12
resolution: "@types/debug@npm:4.1.12"
dependencies:
@@ -3013,6 +3013,15 @@ __metadata:
languageName: node
linkType: hard
"@types/debug@npm:^4.1.13":
version: 4.1.13
resolution: "@types/debug@npm:4.1.13"
dependencies:
"@types/ms": "npm:*"
checksum: 10c0/e5e124021bbdb23a82727eee0a726ae0fc8a3ae1f57253cbcc47497f259afb357de7f6941375e773e1abbfa1604c1555b901a409d762ec2bb4c1612131d4afb7
languageName: node
linkType: hard
"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8":
version: 1.0.8
resolution: "@types/estree@npm:1.0.8"
@@ -4082,7 +4091,7 @@ __metadata:
linkify-string: "npm:^4.3.2"
linkifyjs: "npm:^4.3.2"
lucide-vue-next: "npm:^0.562.0"
mediasoup-client: "npm:^3.18.6"
mediasoup-client: "npm:^3.19.0"
mitt: "npm:^3.0.1"
nuxt: "npm:^4.2.2"
postcss: "npm:^8.5.6"
@@ -7355,11 +7364,11 @@ __metadata:
languageName: node
linkType: hard
"mediasoup-client@npm:^3.18.6":
version: 3.18.6
resolution: "mediasoup-client@npm:3.18.6"
"mediasoup-client@npm:^3.19.0":
version: 3.19.0
resolution: "mediasoup-client@npm:3.19.0"
dependencies:
"@types/debug": "npm:^4.1.12"
"@types/debug": "npm:^4.1.13"
"@types/events-alias": "npm:@types/events@^3.0.3"
awaitqueue: "npm:^3.3.0"
debug: "npm:^4.4.3"
@@ -7368,7 +7377,7 @@ __metadata:
h264-profile-level-id: "npm:^2.3.2"
sdp-transform: "npm:^3.0.0"
supports-color: "npm:^10.2.2"
checksum: 10c0/f5baff9139afccf88de5db767c1139efa5cdd68f4871e2fa9d6ff94d2e71d2365dc40e9ba6e903cde5fbb51a2d82972e738da656be9f6fc7006640fdd82dd5da
checksum: 10c0/9fde5ec5daec91d43a88796f49e2b1b7a018c8100a3f99786966678a0e0b5328e88f6e6af36d50f9eed93889b84f23a164865c7177c0767ee805c7a8c7a51eb2
languageName: node
linkType: hard

3
new-client/.env Normal file
View File

@@ -0,0 +1,3 @@
; API_BASE_URL=/api
; API_BASE_URL=https://api.koptilnya.xyz/chad
API_BASE_URL=http://127.0.0.1:4000

24
new-client/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
new-client/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View File

@@ -0,0 +1,54 @@
{
// Use ESLint's --fix:
"code_actions_on_format": {
"source.fixAll.eslint": true,
},
"formatter": [],
// Enable eslint for all supported languages
// Defaults only include https://github.com/search?q=repo%3Azed-industries%2Fzed%20eslint_languages&type=code
"languages": {
"HTML": {
"language_servers": ["...", "eslint"],
},
"Markdown": {
"language_servers": ["...", "eslint"],
},
"Markdown-Inline": {
"language_servers": ["...", "eslint"],
},
"JSON": {
"language_servers": ["...", "eslint"],
},
"JSONC": {
"language_servers": ["...", "eslint"],
},
"YAML": {
"language_servers": ["...", "eslint"],
},
"CSS": {
"language_servers": ["...", "eslint"],
},
// Add other languages as needed
},
"lsp": {
"eslint": {
"settings": {
"workingDirectories": ["./"],
// Silent the stylistic rules in your IDE, but still auto fix them
"rulesCustomizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true },
],
},
},
},
}

View File

@@ -0,0 +1,17 @@
import antfu from '@antfu/eslint-config'
export default antfu({
formatters: {
css: true,
},
overrides: {
typescript: {
'no-console': 'off',
},
vue: {
'vue/block-order': ['error', {
order: ['template', 'script', 'style'],
}],
},
},
})

24
new-client/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<title>Chad</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/src/shared/styles/reset.css" />
<link rel="stylesheet" href="/src/shared/styles/sanitize.css" />
<link rel="stylesheet" href="/src/shared/styles/main.scss" />
</head>
<body>
<div data-mount-point id="app"></div>
<div data-mount-point id="updater"></div>
<div data-mount-point id="preloader"></div>
<script type="module" src="/src/app/entry.ts"></script>
</body>
</html>

36
new-client/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "new-client",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@ark-ui/vue": "^5.36.2",
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
"@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"@vueuse/core": "^14.3.0",
"@zag-js/vue": "^1.40.0",
"primevue": "^4.5.5",
"vue": "^3.5.32",
"vue-router": "^5.0.6"
},
"devDependencies": {
"@antfu/eslint-config": "^8.2.0",
"@tauri-apps/cli": "^2.8.4",
"@types/node": "^24.12.2",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.3.0",
"eslint-plugin-format": "^2.0.1",
"sass-embedded": "^1.99.0",
"typescript": "~6.0.2",
"vite": "^8.0.10",
"vue-tsc": "^3.2.7"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

4
new-client/src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

6228
new-client/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
[package]
name = "app"
version = "0.1.0"
description = "WW додепчик"
authors = ["KPTL"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.4.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.8.5", features = [] }
tauri-plugin-log = "2"
tauri-plugin-process = "2"
windows = { version = "0.52", features = ["Win32_UI_Shell"] }
tauri-plugin-opener = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Request camera access for WebRTC</string>
<key>NSMicrophoneUsageDescription</key>
<string>Request microphone access for WebRTC</string>
</dict>
</plist>

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}

View File

@@ -0,0 +1,20 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"updater:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"opener:allow-default-urls",
"opener:allow-open-url"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M232.899 20C237.318 20.0002 240.899 23.5819 240.899 28V227.644C240.899 234.599 232.635 238.241 227.501 233.548L202.03 210.261C200.556 208.913 198.63 208.165 196.632 208.165H23.1006C18.6825 208.165 15.1006 204.583 15.1006 200.165V28C15.1006 23.5819 18.6825 20.0002 23.1006 20H232.899ZM129.758 58.3818C120.029 58.3818 111.244 60.5556 103.403 64.9033C95.5633 69.251 89.3442 75.6118 84.7471 83.9863C80.1499 92.3611 77.8516 102.572 77.8516 114.617C77.8516 126.627 80.1147 136.82 84.6406 145.194C89.1665 153.569 95.3323 159.948 103.137 164.331C110.977 168.679 119.851 170.853 129.758 170.853C137.277 170.852 143.941 169.712 149.75 167.432C155.594 165.151 160.548 162.086 164.61 158.237C168.673 154.353 171.863 150.059 174.18 145.354C175.83 142.054 177.015 138.719 177.735 135.349C178.227 133.051 176.378 131.016 174.028 131.002L155.054 130.888C153.103 130.876 151.468 132.29 150.861 134.145C150.415 135.508 149.849 136.786 149.162 137.978C147.986 140.044 146.471 141.808 144.618 143.27C142.801 144.695 140.68 145.782 138.257 146.53C135.869 147.279 133.214 147.653 130.292 147.653C125.089 147.653 120.581 146.424 116.768 143.965C112.99 141.47 110.05 137.782 107.947 132.899C105.88 127.981 104.847 121.887 104.847 114.617C104.847 107.632 105.862 101.681 107.894 96.7627C109.961 91.8448 112.901 88.0849 116.714 85.4834C120.563 82.8819 125.142 81.5811 130.452 81.5811C133.446 81.5811 136.172 82.009 138.631 82.8643C141.125 83.6839 143.282 84.8773 145.1 86.4453C146.917 88.0134 148.378 89.9028 149.482 92.1123C150.104 93.3561 150.607 94.6845 150.992 96.0977C151.52 98.0371 153.178 99.542 155.188 99.542H173.992C176.355 99.5418 178.218 97.5003 177.806 95.1738C176.953 90.3679 175.494 85.9973 173.431 82.0625C170.758 76.9664 167.283 72.6721 163.007 69.1797C158.73 65.6516 153.777 62.9786 148.146 61.1611C142.516 59.308 136.386 58.3819 129.758 58.3818Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,21 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
// .plugin(tauri_plugin_single_instance::init(|_, _, _| {}))
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,21 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[cfg(target_os = "windows")]
fn set_app_user_model_id() {
use windows::core::HSTRING;
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
unsafe {
SetCurrentProcessExplicitAppUserModelID(&HSTRING::from("xyz.koptilnya.chad"))
.ok()
.expect("Failed to set AppUserModelID");
}
}
fn main() {
#[cfg(target_os = "windows")]
set_app_user_model_id();
app_lib::run();
}

View File

@@ -0,0 +1,60 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Chad",
"version": "0.3.0-rc.3",
"identifier": "xyz.koptilnya.chad",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn generate"
},
"app": {
"windows": [
{
"maximizable": true,
"label": "main",
"title": "Chad",
"width": 800,
"height": 600,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"center": true,
"theme": "Dark",
"additionalBrowserArgs": "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required --lang=en",
"incognito": false
}
],
"security": {
"csp": null,
"capabilities": []
}
},
"bundle": {
"createUpdaterArtifacts": true,
"active": true,
"targets": ["nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"nsis": {
"installerIcon": "icons/icon.ico"
}
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3MzkxMzM3RkQ3NTg4QUQKUldTdGlIWDlOeE01NStIak9VbmZTTm9HY2NyNUQrVXB5ZEdIN1BkK2lhYW9zWkNCQnZQSjRmelIK",
"endpoints": [
"https://git.koptilnya.xyz/opti1337/chad/releases/download/latest/updater.json"
]
}
}
}

View File

@@ -0,0 +1,20 @@
<template>
<Component :is="layoutComponent">
<RouterView />
</Component>
</template>
<script setup lang="ts">
import DefaultLayout from '@shared/layouts/Default.vue'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const layoutComponent = computed(() => {
return route.meta.layout ?? DefaultLayout
})
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="error-app">
<img class="error-app__image" src="/sad-pepe.png" alt="Oops!" draggable="false">
<p class="error-app__message">
{{ message }}
</p>
</div>
</template>
<script setup lang="ts">
defineProps<{
message: string
}>()
</script>
<style lang="scss">
.error-app {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
&__image {
width: 120px;
margin-bottom: 32px;
user-select: none;
}
&__message {
color: var(--text-muted);
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div class="preloader-app">
<p>
Loading...
</p>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss">
.preloader-app {
display: flex;
height: 100%;
> p {
margin: auto;
padding: 8px 16px;
border-radius: 16px;
background-color: var(--bg-light);
}
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div class="updater-app">
<p v-if="checking">
Checking updates...
</p>
<p v-else-if="!!lastUpdate">
Update available: {{ lastUpdate.version }}
</p>
</div>
</template>
<script setup lang="ts">
import { useUpdater } from '@shared/composables/use-updater'
const { checking, lastUpdate } = useUpdater()
</script>
<style lang="scss">
.updater-app {
display: flex;
height: 100%;
> p {
margin: auto;
padding: 8px 16px;
border-radius: 16px;
background-color: var(--bg-light);
}
}
</style>

View File

@@ -0,0 +1,21 @@
import { useAuth } from '@shared/composables/use-auth'
import { createApp } from 'vue'
import App from '../App.vue'
import routerPlugin, { router } from '../plugins/router'
const mountPoint = '#app'
export default async function () {
const { authorized } = useAuth()
const app = createApp(App)
app.use(routerPlugin)
await router.isReady()
if (!authorized.value && router.currentRoute.value.meta.auth === undefined) {
router.push('/auth/login')
}
app.mount(mountPoint)
}

View File

@@ -0,0 +1,17 @@
import api from '@shared/api/client'
import { useAuth } from '@shared/composables/use-auth'
export default async function () {
const { setMe } = useAuth()
try {
const response = await api.chad.authMe()
setMe(response.data)
}
catch (error) {
if (error.error?.statusCode !== 401) {
throw new Error('Authorization failed')
}
}
}

View File

@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import Error from '../Error.vue'
const mountPoint = '#app'
export default async function (message: string) {
const app = createApp(Error, { message })
app.mount(mountPoint)
}

View File

@@ -0,0 +1,19 @@
import type { App } from 'vue'
import { createApp } from 'vue'
import Preloader from '../Preloader.vue'
const mountPoint = '#preloader'
let preloaderApp: App | undefined
export default {
show() {
preloaderApp = createApp(Preloader)
preloaderApp.mount(mountPoint)
},
hide() {
preloaderApp?.unmount()
preloaderApp = undefined
},
}

View File

@@ -0,0 +1,23 @@
import { createApp } from 'vue'
import { useUpdater } from '@/shared/composables/use-updater'
import Updater from '../Updater.vue'
const mountPoint = '#updater'
export default async function () {
const { lastUpdate, checkForUpdates } = useUpdater()
const updater = createApp(Updater)
updater.mount(mountPoint)
await checkForUpdates()
if (!lastUpdate.value) {
updater.unmount()
return
}
await lastUpdate.value.downloadAndInstall()
}

View File

@@ -0,0 +1,30 @@
import initializeApp from './bootstrap/app'
import authorize from './bootstrap/authorize'
import showError from './bootstrap/error'
import preloader from './bootstrap/preloader'
import checkUpdates from './bootstrap/updater'
(async () => {
try {
await checkUpdates()
preloader.show()
await authorize()
initializeApp()
}
catch (error) {
console.error(error)
if (error instanceof Error && error.message) {
showError(error.message)
}
else {
showError('Something went wrong')
}
}
finally {
preloader.hide()
}
})()

View File

@@ -0,0 +1,11 @@
import type { PrimeVueConfiguration } from 'primevue/config'
import type { FunctionPlugin, Plugin } from 'vue'
import PrimeVue from 'primevue/config'
export default {
install(app) {
app.use(PrimeVue as Plugin<PrimeVueConfiguration>, {
unstyled: true,
})
},
} as FunctionPlugin

View File

@@ -0,0 +1,21 @@
import type { Component, FunctionPlugin } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'
declare module 'vue-router' {
interface RouteMeta {
layout?: Component
auth?: false | 'guest'
}
}
export const router = createRouter({
history: createWebHistory(),
routes,
})
export default {
install(app) {
app.use(router)
},
} as FunctionPlugin

View File

@@ -0,0 +1,14 @@
<template>
<RouterView />
</template>
<script setup lang="ts">
import AuthLayout from '@shared/layouts/Auth.vue'
definePage({
meta: {
auth: 'guest',
layout: AuthLayout,
},
})
</script>

View File

@@ -0,0 +1,35 @@
<template>
<div class="login-page">
<form class="login-page__form" @submit.prevent="">
<div class="login-page__fields">
<ChadInput placeholder="Login" error="Jopa" />
<ChadInput placeholder="Password" />
<ChadPasswordInput placeholder="Password" label="Test" />
</div>
<ChadButton class="login-page__submit" full type="submit">
Let's go
</ChadButton>
</form>
</div>
</template>
<script setup lang="ts">
import ChadButton from '@shared/components/ui/Button.vue'
import ChadInput from '@shared/components/ui/Input.vue'
import ChadPasswordInput from '@shared/components/ui/PasswordInput.vue'
</script>
<style lang="scss">
.login-page {
&__fields {
> *:not(:last-child) {
margin-bottom: 16px;
}
}
&__submit {
margin-top: 24px;
}
}
</style>

View File

@@ -0,0 +1,6 @@
<template>
Register page
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,5 @@
<template>
Index page
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,11 @@
import { Api } from './generated-chad-api'
const api = new Api({
baseUrl: 'http://localhost:4000',
})
function isChadResponseError(error) {
}
export default api

View File

@@ -0,0 +1,755 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
/**
* Attachment
* Attachment
*/
export interface Attachment {
id: string;
name: string;
mimetype: string;
/** @min 0 */
size: number;
/** @format date-time */
createdAt: string;
}
/**
* Channel
* Channel
*/
export interface Channel {
id: string;
ownerId: string | null;
name: string;
persistent: boolean;
}
/**
* ChatMessage
* ChatMessage
*/
export interface ChatMessage {
/** @format uuid */
id: string;
/** @format uuid */
senderId: string;
/** @minLength 1 */
text: string;
/** @format date-time */
createdAt: string;
/** @format date-time */
updatedAt: string;
attachments: string[];
}
/**
* CreateChannelPayload
* CreateChannelPayload
*/
export interface CreateChannelPayload {
name: string;
persistent: boolean;
}
/**
* CreateUser
* CreateUser
*/
export interface CreateUser {
/** @minLength 1 */
username: string;
/** @minLength 6 */
password: string;
}
/**
* GetAttachmentParams
* GetAttachmentParams
*/
export interface GetAttachmentParams {
/** @format uuid */
id: string;
}
/**
* GetUserQuery
* GetUserQuery
*/
export interface GetUserQuery {
username?: string;
}
/**
* Login
* Login
*/
export interface Login {
/** @minLength 1 */
username: string;
/** @minLength 1 */
password: string;
}
/**
* NewChatMessagePayload
* NewChatMessagePayload
*/
export interface NewChatMessagePayload {
/** @minLength 1 */
text: string;
attachments?: string[];
}
/**
* Reply
* Reply
*/
export interface Reply {
/** @format uuid */
messageId: string;
/** @format uuid */
senderId: string;
text: string;
}
/**
* ResponseError
* ResponseError
*/
export interface ResponseError {
statusCode: number;
error: string;
message: string;
}
/**
* UpdateUserPayload
* UpdateUserPayload
*/
export interface UpdateUserPayload {
displayName: string;
}
/**
* UpdateUserPreferencesPayload
* UpdateUserPreferencesPayload
*/
export interface UpdateUserPreferencesPayload {
toggleInputHotkey?: string;
toggleOutputHotkey?: string;
}
/**
* UserPreferences
* UserPreferences
*/
export interface UserPreferences {
toggleInputHotkey: string;
toggleOutputHotkey: string;
}
/**
* User
* User
*/
export interface User {
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
}
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<
FullRequestParams,
"body" | "method" | "query" | "path"
>;
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (
securityData: SecurityDataType | null,
) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
JsonApi = "application/vnd.api+json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: "same-origin",
headers: {},
redirect: "follow",
referrerPolicy: "no-referrer",
};
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
};
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter(
(key) => "undefined" !== typeof query[key],
);
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key),
)
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string")
? JSON.stringify(input)
: input,
[ContentType.JsonApi]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string")
? JSON.stringify(input)
: input,
[ContentType.Text]: (input: any) =>
input !== null && typeof input !== "string"
? JSON.stringify(input)
: input,
[ContentType.FormData]: (input: any) => {
if (input instanceof FormData) {
return input;
}
return Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob
? property
: typeof property === "object" && property !== null
? JSON.stringify(property)
: `${property}`,
);
return formData;
}, new FormData());
},
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
};
protected mergeRequestParams(
params1: RequestParams,
params2?: RequestParams,
): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (
cancelToken: CancelToken,
): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
};
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
};
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format;
return this.customFetch(
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
{
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData
? { "Content-Type": type }
: {}),
},
signal:
(cancelToken
? this.createAbortSignal(cancelToken)
: requestParams.signal) || null,
body:
typeof body === "undefined" || body === null
? null
: payloadFormatter(body),
},
).then(async (response) => {
const r = response as HttpResponse<T, E>;
r.data = null as unknown as T;
r.error = null as unknown as E;
const responseToParse = responseFormat ? response.clone() : response;
const data = !responseFormat
? r
: await responseToParse[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data;
} else {
r.error = data;
}
return r;
})
.catch((e) => {
r.error = e;
return r;
});
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
if (!response.ok) throw data;
return data;
});
};
}
/**
* @title Chad API
* @version 1.0.0
*/
export class Api<
SecurityDataType extends unknown,
> extends HttpClient<SecurityDataType> {
chad = {
/**
* @description Pass file to multipart/form-data
*
* @tags Attachment
* @name AttachmentUpload
* @summary Upload attachment
* @request POST:/chad/attachment/upload
*/
attachmentUpload: (params: RequestParams = {}) =>
this.request<string, ResponseError>({
path: `/chad/attachment/upload`,
method: "POST",
format: "json",
...params,
}),
/**
* No description
*
* @tags Attachment
* @name AttachmentGet
* @summary Get attachment
* @request GET:/chad/attachment/{id}
*/
attachmentGet: (id: string, params: RequestParams = {}) =>
this.request<any, ResponseError>({
path: `/chad/attachment/${id}`,
method: "GET",
format: "json",
...params,
}),
/**
* No description
*
* @tags Auth
* @name AuthRegister
* @summary Register
* @request POST:/chad/auth/register
*/
authRegister: (data: CreateUser, params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/auth/register`,
method: "POST",
body: data,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Auth
* @name AuthLogin
* @summary Login
* @request POST:/chad/auth/login
*/
authLogin: (data: Login, params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/auth/login`,
method: "POST",
body: data,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Auth
* @name AuthMe
* @summary Me
* @request GET:/chad/auth/me
*/
authMe: (params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/auth/me`,
method: "GET",
format: "json",
...params,
}),
/**
* No description
*
* @tags Auth
* @name AuthLogout
* @summary Logout
* @request POST:/chad/auth/logout
*/
authLogout: (params: RequestParams = {}) =>
this.request<any, ResponseError>({
path: `/chad/auth/logout`,
method: "POST",
...params,
}),
/**
* No description
*
* @tags Channel
* @name ChannelList
* @summary Get channel list
* @request GET:/chad/channels
*/
channelList: (params: RequestParams = {}) =>
this.request<Channel[], ResponseError>({
path: `/chad/channels`,
method: "GET",
format: "json",
...params,
}),
/**
* No description
*
* @tags Channel
* @name ChannelCreate
* @summary Create channel
* @request POST:/chad/channels
*/
channelCreate: (data: CreateChannelPayload, params: RequestParams = {}) =>
this.request<Channel, ResponseError>({
path: `/chad/channels`,
method: "POST",
body: data,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Channel
* @name ChannelDelete
* @summary Delete channel
* @request DELETE:/chad/channels/{id}
*/
channelDelete: (id: string, params: RequestParams = {}) =>
this.request<any, ResponseError>({
path: `/chad/channels/${id}`,
method: "DELETE",
...params,
}),
/**
* No description
*
* @tags Chat
* @name ChatSend
* @summary Send message
* @request POST:/chad/chat/send
*/
chatSend: (
data: {
/** @minLength 1 */
text: string;
attachments?: string[];
},
params: RequestParams = {},
) =>
this.request<
{
/** @format uuid */
id: string;
/** @format uuid */
senderId: string;
/** @minLength 1 */
text: string;
/** @format date-time */
createdAt: string;
/** @format date-time */
updatedAt: string;
attachments: string[];
},
ResponseError
>({
path: `/chad/chat/send`,
method: "POST",
body: data,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Chat
* @name ChatMessages
* @summary Get messages
* @request GET:/chad/chat
*/
chatMessages: (
query: {
/**
* Cursor to message
* @format uuid
*/
cursor?: string;
/**
* @min 1
* @max 100
* @default 10
*/
limit: number;
},
params: RequestParams = {},
) =>
this.request<
{
messages: {
/** @format uuid */
id: string;
/** @format uuid */
senderId: string;
/** @minLength 1 */
text: string;
/** @format date-time */
createdAt: string;
/** @format date-time */
updatedAt: string;
attachments: string[];
}[];
/**
* Cursor to last message
* @format uuid
*/
nextCursor?: string;
},
ResponseError
>({
path: `/chad/chat`,
method: "GET",
query: query,
format: "json",
...params,
}),
/**
* No description
*
* @tags User
* @name UserGet
* @summary Get user
* @request GET:/chad/user
*/
userGet: (
query?: {
username?: string;
},
params: RequestParams = {},
) =>
this.request<User, ResponseError>({
path: `/chad/user`,
method: "GET",
query: query,
format: "json",
...params,
}),
/**
* No description
*
* @tags User
* @name UserGetPreferences
* @summary Get preferences
* @request GET:/chad/user/preferences
*/
userGetPreferences: (params: RequestParams = {}) =>
this.request<UserPreferences, ResponseError>({
path: `/chad/user/preferences`,
method: "GET",
format: "json",
...params,
}),
/**
* No description
*
* @tags User
* @name UserUpdatePreferences
* @summary Update preferences
* @request PATCH:/chad/user/preferences
*/
userUpdatePreferences: (
data: UpdateUserPreferencesPayload,
params: RequestParams = {},
) =>
this.request<any, ResponseError>({
path: `/chad/user/preferences`,
method: "PATCH",
body: data,
type: ContentType.Json,
...params,
}),
/**
* No description
*
* @tags User
* @name UserUpdateProfile
* @summary Update profile
* @request PATCH:/chad/profile
*/
userUpdateProfile: (data: UpdateUserPayload, params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/profile`,
method: "PATCH",
body: data,
type: ContentType.Json,
format: "json",
...params,
}),
};
}

View File

@@ -0,0 +1,31 @@
<template>
<div class="app-logo">
<p class="app-logo__title">
Chad
</p>
<p class="app-logo__version">
{{ version }}
</p>
</div>
</template>
<script setup lang="ts">
import { useApp } from '@shared/composables/use-app'
const { version } = useApp()
</script>
<style lang="scss">
.app-logo {
&__title {
color: var(--primary);
font-weight: 700;
line-height: 20px;
}
&__version {
font-size: 0.75rem;
color: var(--text-muted);
}
}
</style>

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