switch to typescript on wails.io

This commit is contained in:
Sam Hoffman
2026-01-17 23:03:24 -05:00
parent d66602cc7d
commit 5e71721f4d
74 changed files with 9273 additions and 3670 deletions

View File

@@ -0,0 +1,81 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"ShallowRef": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"getCurrentWatcher": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"isShallow": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useModel": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -0,0 +1,3 @@
import vuetify from 'eslint-config-vuetify'
export default vuetify()

13
app/frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>IrChad</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>

7169
app/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
app/frontend/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "irchad",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/roboto": "^5.2.9",
"@mdi/font": "^7.4.47",
"buffer": "^6.0.3",
"irc-framework": "^4.14.0",
"pinia": "^3.0.4",
"vite-plugin-node-polyfills": "^0.25.0",
"vue": "^3.2.37",
"vuetify": "^3.11.6"
},
"devDependencies": {
"@babel/types": "^7.18.10",
"@tsconfig/node22": "^22.0.5",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
"eslint-config-vuetify": "^4.3.5-beta.1",
"sass-embedded": "^1.97.2",
"typescript": "^5.9.3",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.3.1",
"vite-plugin-vuetify": "^2.1.2",
"vue-tsc": "^3.2.2"
}
}

1
app/frontend/package.json.md5 Executable file
View File

@@ -0,0 +1 @@
d86c613036f8e3460cc743ac40acec6e

14
app/frontend/src/App.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<v-app>
<Login v-if="!ircStore.connected" />
<Chat v-else />
</v-app>
</template>
<script lang="ts" setup>
import { useIRCStore } from "@/stores/irc";
import Chat from "@/components/Chat.vue";
import Login from "@/components/Login.vue";
const ircStore = useIRCStore();
</script>

View File

@@ -0,0 +1,24 @@
<script setup>
import { useBufferStore } from "@/stores/bufferStore";
const { setActiveBuffer, buffers, activeBufferName } = useBufferStore();
const store = useBufferStore();
</script>
<template>
<v-list
selectable
:selected="[activeBufferName]"
@click:select="(item) => setActiveBuffer(item.id)"
>
<v-list-item :value="bufName" v-for="(bufValue, bufName) in buffers">
{{ bufName }}
<template v-slot:append>
<v-badge
:content="bufValue.messages.length - bufValue.lastSeenIdx"
inline
/>
</template>
</v-list-item>
</v-list>
</template>

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import { useIRCStore } from "@/stores/irc";
import { useBufferStore } from "@/stores/bufferStore";
import { useAccountStore } from "@/stores/accountStore";
const bufferStore = useBufferStore();
const ircStore = useIRCStore();
const accountStore = useAccountStore();
</script>
<template>
<div class="d-flex flex-row" style="height: 100vh">
<v-sheet border class="buffers">
<UserCard />
<v-divider />
<BufferList />
</v-sheet>
<div class="messages d-flex flex-column">
<v-toolbar density="compact">
<v-toolbar-title>
<p>{{ bufferStore.activeBufferName }}</p>
{{ bufferStore.activeBuffer?.topic }}
</v-toolbar-title>
</v-toolbar>
<MessageList
:messages="bufferStore.activeBuffer?.messages"
:me="accountStore.account.nick"
/>
<v-sheet>
<!-- <v-text-field -->
<!-- variant="outlined" -->
<!-- :placeholder="`Message ${store.activeBufferName}`" -->
<!-- v-model="inputBuffer" -->
<!-- hide-details -->
<!-- class="ma-2" -->
<!-- @keydown.enter.exact.prevent="send" -->
<!-- /> -->
<InputBuffer @send="ircStore.sendActiveBuffer" />
</v-sheet>
</div>
<v-sheet class="user-list h-100" border>
<UserList :users="bufferStore.activeBuffer?.users" />
</v-sheet>
</div>
</template>
<style>
.buffers {
height: 100%;
flex: 1;
}
.messages {
height: 100%;
flex: 3;
justify-content: space-between;
}
.user-list {
height: 100%;
flex: 1;
}
</style>

View File

@@ -0,0 +1,104 @@
<script setup>
import { ref } from "vue";
import { useIRCStore } from "@/stores/irc";
import { useBufferStore } from "@/stores/bufferStore";
const emit = defineEmits(["send"]);
const store = useIRCStore();
const bufferStore = useBufferStore();
const text = ref();
const menu = ref({
open: false,
activator: null,
});
const menuList = ref({
density: "compact",
slim: true,
items: [],
itemTitle: "title",
itemValue: "value",
selected: [],
selectable: true,
mandatory: true,
returnObject: true,
});
const cursorPos = ref(0);
const completionPos = ref(0);
function clickItem() {}
function send() {
if (!text.value) return;
emit("send", text.value);
menu.value.open = false;
text.value = "";
}
function filterUsers(s) {
if (store.activeBuffer) {
return store.activeBuffer.users.filter((u) => u.nick.startsWith(s));
}
}
function trigger(ev) {
const input = ev.target;
const cursor = input.selectionStart;
cursorPos.value = cursor;
const text = input.value;
const textBefore = text.slice(0, cursor);
const mentionMatch = textBefore.match(/@(\w*)$/);
if (mentionMatch) {
menu.value.open = true;
menuList.value.items = filterUsers(mentionMatch[1]);
menuList.value.itemTitle = "nick";
menuList.value.itemValue = "nick";
} else {
menu.value.open = false;
}
}
function tabComplete() {
if (!menu.value.open) {
return;
}
let nextSelection = 0;
if (menuList.value.selected.length) {
const currentIdx = menuList.value.items.indexOf(menuList.value.selected[0]);
const nextIdx = currentIdx + 1;
if (menuList.value.items[nextIdx]) nextSelection = nextIdx;
}
menuList.value.selected = [menuList.value.items[nextSelection]];
// hello @
const beforeCursor = text.value.splice(0, cursor);
const afterCursor = text.value.slice(cursor);
}
</script>
<template>
<v-menu
v-model="menu.open"
location="top"
activator="parent"
:open-on-click="false"
:open-on-focus="false"
>
<v-list v-bind="menuList" @click:select="clickItem" />
</v-menu>
<v-text-field
v-model="text"
autofocus
hide-details
:placeholder="`Message ${bufferStore.activeBufferName}`"
@input="trigger"
@keydown.enter.prevent="send"
@keydown.tab.prevent="tabComplete"
variant="outlined"
class="ma-1"
role="irchad"
></v-text-field>
</template>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { useAccountStore } from "@/stores/accountStore";
import { useIRCStore } from "@/stores/irc";
import { storeToRefs } from "pinia";
const accountStore = useAccountStore();
const ircStore = useIRCStore();
const { account } = storeToRefs(accountStore);
const withAccount = ref(false);
const form = ref(false);
function login() {
ircStore.connect();
}
function required(v: any) {
return !!v || "This field is required";
}
</script>
<template>
<main class="w-25 mt-5 ma-auto">
<v-card title="Login to IrChad">
<v-form @submit.prevent="login" v-model="form">
<v-card-text>
<v-text-field
v-model="account.nick"
label="Nickname"
:rules="[required]"
/>
<v-text-field
v-if="withAccount"
v-model="account.account"
label="Username"
role="username"
:rules="[required]"
/>
<v-text-field
v-model="account.password"
v-if="withAccount"
label="Password"
type="password"
:rules="[required]"
/>
<v-alert color="error" v-if="accountStore.authError.reason">
{{ accountStore.authError.message }}
</v-alert>
<v-checkbox v-model="withAccount" label="Login with an account" />
</v-card-text>
<v-card-actions>
<v-btn type="submit" color="success" :disabled="!form">Connect</v-btn>
</v-card-actions>
</v-form>
</v-card>
</main>
</template>

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import { computed, watch, useTemplateRef } from "vue";
import { useIRCStore } from "@/stores/irc";
const props = defineProps(["messages", "me"]);
const store = useIRCStore();
const messagesReverse = computed(() => {
if (props.messages) {
return [...props.messages];
}
});
const timeFormatter = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
function formatTime(ts: string) {
const date = new Date(ts);
return timeFormatter.format(date);
}
const chatHistory = useTemplateRef("chat-scrollback");
watch(
() => props.messages,
() =>
nextTick(() => {
chatHistory.value!.scrollTop = chatHistory.value!.scrollHeight;
}),
{ deep: true },
);
</script>
<template>
<v-sheet ref="chat-history" class="message-list d-flex">
<div ref="chat-scrollback">
<v-virtual-scroll height="100%" :items="messagesReverse">
<template #default="{ item: msg }">
<v-list-item
density="compact"
:prepend-avatar="store.getMetadata(msg.nick, 'avatar')"
>
<v-list-item-title>
<span
class="message-nick font-weight-bold"
:class="{ 'text-primary': me === msg.nick }"
>
{{ msg.nick }}
</span>
<span class="message-time" v-if="!!msg.time">{{
formatTime(msg.time)
}}</span>
</v-list-item-title>
{{ msg.message }}
</v-list-item></template
>
</v-virtual-scroll>
</div>
</v-sheet>
</template>
<style scoped>
.message-list {
height: 100%;
overflow-y: auto;
flex-direction: column-reverse;
}
.message-time {
font-size: 0.65em;
margin-left: 4px;
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import { ref } from "vue";
import { useIRCStore } from "@/stores/irc";
import { storeToRefs } from "pinia";
import { useAccountStore } from "@/stores/accountStore";
const ircStore = useIRCStore();
const accountStore = useAccountStore();
const { selfAvatar } = storeToRefs(ircStore);
const avatarDialog = ref(false);
const newNick = ref();
const newBio = ref();
function changeAvatar() {
newNick.value = accountStore.account.nick;
avatarDialog.value = true;
}
function submitAvatar() {
ircStore.setAvatar(selfAvatar.value);
avatarDialog.value = false;
if (newNick.value && accountStore.account.nick !== newNick.value) {
ircStore.setNick(newNick.value);
}
if (newBio.value) {
ircStore.setBio(newBio.value);
}
}
</script>
<template>
<v-dialog v-model="avatarDialog" max-width="800px">
<v-card title="Edit Profile">
<v-card-text>
<v-text-field v-model="selfAvatar" label="Avatar URL" />
<v-text-field v-model="newNick" label="Nick" />
<v-text-field v-model="newBio" label="Bio" />
</v-card-text>
<v-card-actions>
<v-btn text="OK" @click="submitAvatar" />
</v-card-actions>
</v-card>
</v-dialog>
<v-card>
<v-card-title>
<v-avatar @click="changeAvatar" v-if="selfAvatar" :image="selfAvatar" />
{{ accountStore.account.nick }}
</v-card-title>
<v-card-text> </v-card-text>
</v-card>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { useBufferStore } from "@/stores/bufferStore";
import { useIRCStore } from "@/stores/irc";
import { computed } from "vue";
const props = defineProps(["users"]);
const store = useIRCStore();
const bufferStore = useBufferStore();
const sortedUsers = computed(() => {
if (!bufferStore.activeBuffer || !bufferStore.activeBuffer.users) return [];
const u = [...bufferStore.activeBuffer.users];
u.sort((a, b) => a.nick.localeCompare(b.nick));
return u;
});
</script>
<template>
<v-list density="compact">
<v-list-item
v-for="user in sortedUsers"
:prepend-avatar="store.getMetadata(user.nick, 'avatar')"
:title="user.nick"
>
<v-menu activator="parent">
<v-card :title="user.nick">
<v-card-text>
<p v-text="store.metadata[user.nick]?.bio"></p>
</v-card-text>
<v-list density="compact">
<v-list-item title="Ident"> {{ user.ident }}</v-list-item>
<v-list-item title="Hostname"> {{ user.hostname }}</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-list-item>
</v-list>
</template>

View File

@@ -0,0 +1,51 @@
"use strict";
import IrcChannel from "irc-framework/src/channel";
import IrcUser from "irc-framework/src/user";
export interface BufferOptions {
channel: typeof IrcChannel;
name: string;
metadata?: Record<string, any>;
}
export class Buffer {
name: string;
options: BufferOptions;
channel: IrcChannel;
metadata: Record<string, any>;
lastSeenIdx: number;
messages: any[];
typing: string[];
users: IrcUser[];
topic: string | null;
constructor(options: BufferOptions) {
this.options = options || null;
this.name = options.name;
this.channel = options.channel || null;
this.metadata = options.metadata || {};
this.messages = [];
this.lastSeenIdx = 0;
this.typing = [];
this.topic = options.topic || null;
if (this.channel) this.syncUsers();
}
syncUsers() {
this.users = [...this.channel.users];
}
kind() {
return this.name.startsWith("#") ? "channel" : "pm";
}
resetLastSeen() {
this.lastSeenIdx = 0;
}
}

16
app/frontend/src/main.ts Normal file
View File

@@ -0,0 +1,16 @@
import { registerPlugins } from "@/plugins";
// Components
import App from "./App.vue";
// Composables
import { createApp } from "vue";
// Styles
// import "unfonts.css";
const app = createApp(App);
registerPlugins(app);
app.mount("#app");

View File

@@ -0,0 +1,8 @@
import vuetify from "./vuetify";
import pinia from "@/stores";
import type { App } from "vue";
export function registerPlugins(app: App) {
app.use(vuetify).use(pinia);
}

View File

@@ -0,0 +1,10 @@
import "@mdi/font/css/materialdesignicons.css";
import "vuetify/styles";
import { createVuetify } from "vuetify";
export default createVuetify({
theme: {
defaultTheme: "system",
},
});

View File

@@ -0,0 +1,25 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useAccountStore = defineStore("accountStore", () => {
const authenticated = ref(false);
const account = ref({
nick: "",
account: "",
password: "",
});
const authError = ref({
reason: "",
message: "",
});
function setAuthenticated(v: boolean) {
authenticated.value = v;
}
function setNick(v: string) {
account.value.nick = v;
}
return { account, authError, authenticated, setAuthenticated, setNick };
});

View File

@@ -0,0 +1,47 @@
import { Buffer, type BufferOptions } from "@/lib/buffer.ts";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
export const useBufferStore = defineStore("bufferStore", () => {
const buffers = ref({} as Record<string, Buffer>);
const activeBufferName = ref(null as string | null);
function setActiveBuffer(bufferName: string) {
const buffer = getBuffer(bufferName);
if (!buffer) return;
activeBufferName.value = bufferName;
buffer.resetLastSeen();
}
function addBuffer(bufferName: string, options: BufferOptions) {
buffers.value[bufferName] = new Buffer(options);
return buffers.value[bufferName];
}
function getBuffer(bufferName: string) {
return buffers.value[bufferName];
}
function delBuffer(bufferName: string) {
if (buffers.value[bufferName]) {
delete buffers.value[bufferName];
}
}
const activeBuffer = computed(() => {
if (activeBufferName.value) {
return buffers.value[activeBufferName.value];
}
});
return {
buffers,
activeBufferName,
activeBuffer,
addBuffer,
getBuffer,
delBuffer,
setActiveBuffer,
};
});

View File

@@ -0,0 +1,3 @@
import { createPinia } from "pinia";
export default createPinia();

View File

@@ -0,0 +1,298 @@
import { defineStore, storeToRefs } from "pinia";
import { Client } from "irc-framework";
import { useBufferStore } from "./bufferStore";
import { ref } from "vue";
import { useAccountStore } from "./accountStore";
export const useIRCStore = defineStore("ircStore", () => {
const bufferStore = useBufferStore();
const accountStore = useAccountStore();
const connected = ref(false);
const { authError } = storeToRefs(accountStore);
const selfAvatar = ref("https://placekittens.com/128/128");
const bio = ref();
loadPrefs();
function setAvatar(v: string) {
selfAvatar.value = v;
client.raw(`METADATA * SET avatar ${selfAvatar.value}`);
storePrefs();
}
function loadPrefs() {
// const v = localStorage.getItem("prefs");
// if (v === null) return;
//
// const prefs = JSON.parse(v);
// if (!prefs) return;
//
// if (prefs.avatar) {
// selfAvatar.value = prefs.avatar;
// }
//
// if (prefs.nick) {
// clientInfo.value.nick = prefs.nick;
// }
//
// if (prefs.bio) {
// bio.value = prefs.bio;
// }
}
function storePrefs() {
// localStorage.setItem(
// "prefs",
// JSON.stringify({
// nick: clientInfo.value.nick,
// avatar: selfAvatar.value,
// bio: bio.value,
// }),
// );
}
function setNick(v: string) {
client.changeNick(v);
}
const metadata = ref({} as Record<string, any>);
function getMetadata(subject: string, key: string) {
if (metadata.value[subject]) return metadata.value[subject][key];
}
const client = markRaw(new Client());
function connect() {
client.requestCap("draft/metadata-2");
client.requestCap("echo-message");
client.requestCap("chathistory");
const tls = location.protocol === "https:";
const connectParams = {
host: location.hostname,
port: location.port,
tls,
version: "irchad on irc-framework",
path: "/ws",
account: accountStore.account.account
? {
account: accountStore.account.account,
password: accountStore.account.password,
}
: undefined,
sasl_disconect_on_fail: true,
nick: accountStore.account.nick,
};
console.log(connectParams);
client.connect(connectParams);
}
function sendActiveBuffer(message: string) {
if (!bufferStore.activeBuffer) {
return;
}
bufferStore.activeBuffer.channel.say(message);
}
function isMe(target: string) {
console.log(client.user.nick);
return target === client.user.nick;
}
function setBio(v: string) {
bio.value = v;
client.raw(`METADATA * SET bio ${bio.value}`);
storePrefs();
}
client.on("socket close", () => {
connected.value = false;
});
client.on("loggedin", () => {
accountStore.setAuthenticated(true);
authError.value = {
reason: "",
message: "",
};
});
client.on(
"sasl failed",
({ reason, message }: { reason: string; message: string }) => {
authError.value = {
reason,
message,
};
},
);
client.on("registered", function () {
connected.value = true;
client.list();
client.raw("METADATA * SUB avatar");
client.raw("METADATA * SUB bio");
client.raw(`METADATA * SET avatar ${selfAvatar.value}`);
client.raw(`METADATA * SET bio ${bio.value}`);
});
client.on(
"nick",
function ({ nick, new_nick }: { nick: string; new_nick: string }) {
if (nick === client.user.nick) {
accountStore.setNick(new_nick);
}
for (let buffName in bufferStore.buffers.value) {
const buff = bufferStore.getBuffer(buffName);
if (!buff) {
console.log(`${buffName} not found`);
continue;
}
const idx = buff.users.findIndex((u) => u.nick === nick);
if (idx === -1) {
console.log(`${nick} not found in ${buffName}`);
continue;
}
buff.users[idx].nick = new_nick;
}
metadata.value[new_nick] = { ...metadata.value[nick] };
delete metadata.value[nick];
},
);
client.on(
"unknown command",
function (ircCommand: { command: string; params: string[] }) {
if (ircCommand.command === "METADATA") {
const from = ircCommand.params[0];
const target = ircCommand.params[2];
const key = ircCommand.params[1];
const value = ircCommand.params[3];
let subject = target;
if (target === "*") {
subject = from;
}
if (!subject || !key || !value) return;
if (!metadata.value[subject]) {
metadata.value[subject] = {};
}
metadata.value[subject][key] = value;
}
},
);
client.on("channel list", (channels: { channel: string }[]) =>
channels.map((ch) => client.join(ch.channel)),
);
client.on(
"tagmsg",
({
nick,
tags,
target,
}: {
nick: string;
tags: string[];
target: string;
}) => {
console.log(nick, tags, target);
},
);
client.on("message", function (message: { nick: string; target: string }) {
let buffer;
if (message.nick === "HistServ") return;
if (isMe(message.target)) {
buffer = bufferStore.getBuffer(message.nick);
} else {
buffer = bufferStore.getBuffer(message.target);
}
if (!buffer) {
return;
}
buffer.messages.push(message);
if (
bufferStore.activeBuffer &&
bufferStore.activeBuffer.name === buffer.name
) {
buffer.resetLastSeen();
}
});
client.on("join", ({ nick, channel }: { nick: string; channel: string }) => {
if (isMe(nick)) {
bufferStore.addBuffer(channel, {
name: channel,
channel: client.channel(channel),
});
if (!bufferStore.activeBuffer) {
bufferStore.setActiveBuffer(channel);
}
client.raw("CHATHISTORY LATEST " + channel + " * 200");
return;
}
const buffer = bufferStore.getBuffer(channel);
if (!buffer) return;
buffer.syncUsers();
buffer.users.push({
nick: nick,
});
});
client.on("quit", function ({ nick }: { nick: string }) {
for (let buff of Object.values(bufferStore.buffers)) {
const idx = buff.users.findIndex((u) => u.nick === nick);
if (idx === -1) continue;
buff.users.splice(idx, 1);
}
});
client.on(
"topic",
({ topic, channel }: { topic: string; channel: string }) => {
const buffer = bufferStore.getBuffer(channel);
if (!buffer) return;
buffer.topic = topic;
},
);
client.on("part", ({ nick, channel }: { nick: string; channel: string }) => {
if (isMe(nick)) {
bufferStore.delBuffer(channel);
}
const buffer = bufferStore.getBuffer(channel);
if (!buffer) return;
const idx = buffer.users.findIndex((u) => u.nick === nick);
if (idx === -1) return;
buffer.users.splice(idx, 1);
});
client.on("userlist", (ev: { channel: string; users: any[] }) => {
const buffer = bufferStore.getBuffer(ev.channel);
if (!buffer) return;
buffer.users = ev.users;
});
return {
connect,
client,
sendActiveBuffer,
getMetadata,
metadata,
selfAvatar,
setAvatar,
setNick,
setBio,
bio,
connected,
};
});

View File

@@ -0,0 +1,26 @@
html {
background-color: rgba(27, 38, 54, 1);
text-align: center;
color: white;
}
body {
margin: 0;
color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local(""),
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
}
#app {
height: 100vh;
text-align: center;
}

View File

View File

@@ -0,0 +1,35 @@
declare module "irc-framework" {
export interface IrcUser {
nick: string;
ident?: string;
hostname?: string;
modes?: string[];
[key: string]: any;
}
//
// export class Client {
// join(channel: string, key?: string);
// constructor();
// connect(options: any): void;
// on(event: string, callback: (event: any) => void): void;
// channel(name: string): IrcChannel;
// changeNick(newNick: string);
// say(target: string, message: string);
// raw(v: string);
// requestCap(cap: string);
// list();
// }
// }
//
// declare module "irc-framework/src/channel" {
// export class IrcChannel {
// constructor(irc_client: Client, channel_name: string, key?: string);
// name: string;
// users: IrcUser[];
// say(message: string): void;
// notice(message: string): void;
// part(message?: string): void;
// join(key?: string): void;
// mode(mode: string, param?: string): void;
// }
}

7
app/frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type {DefineComponent} from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -0,0 +1,20 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"],
"allowJs": true
}
}

View File

@@ -0,0 +1,62 @@
import Vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import Vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
plugins: [
Vue({
template: { transformAssetUrls },
}),
AutoImport({
imports: [
"vue",
{
pinia: ["defineStore", "storeToRefs"],
},
],
dts: "src/auto-imports.d.ts",
eslintrc: {
enabled: true,
},
vueTemplate: true,
}),
Components({
dts: "src/components.d.ts",
}),
Vuetify({
autoImport: true,
styles: {
configFile: "src/styles/settings.scss",
},
}),
nodePolyfills({
globals: {
Buffer: true,
},
}),
],
optimizeDeps: {
exclude: ["vuetify"],
},
define: { "process.env": {} },
resolve: {
alias: {
"@": fileURLToPath(new URL("src", import.meta.url)),
},
extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"],
},
server: {
port: 3000,
proxy: {
"/ws": {
target: "ws://localhost:8097",
ws: "true",
},
},
},
});

4
app/frontend/wailsjs/go/main/App.d.ts vendored Executable file
View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1:string):Promise<string>;

View File

@@ -0,0 +1,7 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}

View File

@@ -0,0 +1,24 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

View File

@@ -0,0 +1,249 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void

View File

@@ -0,0 +1,242 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}