feat(ui): complete modular main UI skeleton (guild list, channels, chat area, members, voice bar)
This commit is contained in:
parent
a3650a8354
commit
9a248071c7
13 changed files with 1569 additions and 23 deletions
34
CLAUDE.md
Normal file
34
CLAUDE.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Aether Project Rules for Claude Code
|
||||
|
||||
You are working on **Project Aether** — a beautiful, fast, native open-source Qt 6 client for the self-hosted Stoat backend.
|
||||
|
||||
**Non-negotiable rules (always follow):**
|
||||
|
||||
- Every source file (.cpp, .qml, CMakeLists.txt) must start with the exact AGPL-3.0 SPDX header:
|
||||
```qml
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
Keep the modular QML structure: separate files in src/qml/components/, src/qml/styles/, src/qml/utils/.
|
||||
Every QML file must import:
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
Theme and Constants are singletons — always use Theme.xxx and Constants.xxx. Never use hard-coded colors or sizes.
|
||||
Never use letterSpacing — use font.letterSpacing.
|
||||
Never use modelData directly in Repeater — always use model.modelData or named roles.
|
||||
When fixing errors, change only the necessary lines. Do not rewrite whole files unless asked.
|
||||
Keep code clean, well-commented, and easily changeable later.
|
||||
|
||||
This is the permanent rule set for Aether. Follow it strictly in every response.
|
||||
|
|
@ -20,11 +20,33 @@ qt_add_executable(aether
|
|||
main.cpp
|
||||
)
|
||||
|
||||
# Mark singletons BEFORE qt_add_qml_module so the generated qmldir
|
||||
# contains the required "singleton" keyword for each type.
|
||||
set_source_files_properties(
|
||||
qml/styles/Theme.qml
|
||||
PROPERTIES QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
qml/utils/Constants.qml
|
||||
PROPERTIES QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
qt_add_qml_module(aether
|
||||
URI Aether
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
qml/main.qml
|
||||
qml/MainWindow.qml
|
||||
qml/components/GuildList.qml
|
||||
qml/components/ChannelList.qml
|
||||
qml/components/ChatArea.qml
|
||||
qml/components/MessageList.qml
|
||||
qml/components/MessageInput.qml
|
||||
qml/components/MemberList.qml
|
||||
qml/components/VoiceBar.qml
|
||||
qml/styles/Theme.qml
|
||||
qml/utils/Constants.qml
|
||||
)
|
||||
|
||||
target_link_libraries(aether PRIVATE
|
||||
|
|
|
|||
94
src/qml/MainWindow.qml
Normal file
94
src/qml/MainWindow.qml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Root application window.
|
||||
/// Layout: [GuildList] | [ChannelList + VoiceBar] | SplitView([ChatArea] | [MemberList])
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
title: Constants.appName
|
||||
width: Constants.minWindowWidth
|
||||
height: Constants.minWindowHeight
|
||||
minimumWidth: Constants.minWindowWidth
|
||||
minimumHeight: Constants.minWindowHeight
|
||||
visible: true
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.DeepPurple
|
||||
Material.background: Theme.bgBase
|
||||
|
||||
background: Rectangle { color: Theme.bgBase }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// ── 1. Guild strip (fixed width, not resizable) ───────────────────────
|
||||
GuildList {
|
||||
Layout.preferredWidth: Constants.guildListWidth
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// ── 2. Channel list + voice bar (fixed width) ─────────────────────────
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: Constants.channelListWidth
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
ChannelList {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
VoiceBar {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Constants.voiceBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Resizable split: chat area + member list ───────────────────────
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
// Minimal 1 px handle that brightens on hover
|
||||
handle: Rectangle {
|
||||
implicitWidth: 1
|
||||
color: SplitHandle.hovered ? Theme.accent : Theme.separator
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
ChatArea {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumWidth: 400
|
||||
}
|
||||
|
||||
MemberList {
|
||||
SplitView.preferredWidth: Constants.memberListWidth
|
||||
SplitView.minimumWidth: 160
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
298
src/qml/components/ChannelList.qml
Normal file
298
src/qml/components/ChannelList.qml
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Channel list panel: server name header + collapsible Text / Voice sections.
|
||||
Rectangle {
|
||||
id: root
|
||||
color: Theme.bgMantle
|
||||
|
||||
property int selectedChannel: 0
|
||||
property string serverName: "Stoat HQ"
|
||||
|
||||
// ── Placeholder channel data ──────────────────────────────────────────────
|
||||
// ListModel avoids JS-array modelData scoping issues; channelId used
|
||||
// instead of id (id is a reserved word in QML ListElement context).
|
||||
ListModel {
|
||||
id: textChannelModel
|
||||
ListElement { channelId: 0; name: "general"; icon: "#"; hasUnread: false }
|
||||
ListElement { channelId: 1; name: "random"; icon: "#"; hasUnread: true }
|
||||
ListElement { channelId: 2; name: "announcements"; icon: "#"; hasUnread: false }
|
||||
ListElement { channelId: 3; name: "dev-talk"; icon: "#"; hasUnread: false }
|
||||
}
|
||||
ListModel {
|
||||
id: voiceChannelModel
|
||||
ListElement { channelId: 10; name: "General"; icon: "🔊"; members: 3 }
|
||||
ListElement { channelId: 11; name: "Gaming"; icon: "🔊"; members: 0 }
|
||||
ListElement { channelId: 12; name: "Music"; icon: "🔊"; members: 1 }
|
||||
}
|
||||
|
||||
property bool textSectionOpen: true
|
||||
property bool voiceSectionOpen: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// ── Server name header ────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Constants.headerHeight
|
||||
color: Theme.bgMantle
|
||||
|
||||
// Subtle bottom shadow line
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: Theme.separator
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp16
|
||||
anchors.rightMargin: Theme.sp12
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.serverName
|
||||
font.pixelSize: Theme.font16
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textPrimary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Settings / chevron icon placeholder
|
||||
Text {
|
||||
text: "⌄"
|
||||
font.pixelSize: Theme.font16
|
||||
color: Theme.textMuted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scrollable channel sections ───────────────────────────────────────
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ColumnLayout {
|
||||
width: root.width
|
||||
spacing: 0
|
||||
|
||||
Item { height: Theme.sp8; width: 1 }
|
||||
|
||||
// ── TEXT CHANNELS section ─────────────────────────────────────
|
||||
SectionHeader {
|
||||
width: root.width
|
||||
label: "TEXT CHANNELS"
|
||||
isOpen: root.textSectionOpen
|
||||
onToggle: root.textSectionOpen = !root.textSectionOpen
|
||||
}
|
||||
|
||||
Column {
|
||||
width: root.width
|
||||
visible: root.textSectionOpen
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: textChannelModel
|
||||
delegate: Item {
|
||||
// Capture model roles here — plain Item delegates
|
||||
// have full model context; inline components do not.
|
||||
readonly property int _cid: model.channelId
|
||||
readonly property string _name: model.name
|
||||
readonly property string _icon: model.icon
|
||||
readonly property bool _hasUnread: model.hasUnread
|
||||
|
||||
width: root.width
|
||||
height: 34
|
||||
|
||||
ChannelItem {
|
||||
width: root.width
|
||||
channelId: _cid
|
||||
name: _name
|
||||
icon: _icon
|
||||
hasUnread: _hasUnread
|
||||
isSelected: root.selectedChannel === _cid
|
||||
onClicked: root.selectedChannel = _cid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: Theme.sp8; width: 1 }
|
||||
|
||||
// ── VOICE CHANNELS section ────────────────────────────────────
|
||||
SectionHeader {
|
||||
width: root.width
|
||||
label: "VOICE CHANNELS"
|
||||
isOpen: root.voiceSectionOpen
|
||||
onToggle: root.voiceSectionOpen = !root.voiceSectionOpen
|
||||
}
|
||||
|
||||
Column {
|
||||
width: root.width
|
||||
visible: root.voiceSectionOpen
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: voiceChannelModel
|
||||
delegate: Item {
|
||||
readonly property int _cid: model.channelId
|
||||
readonly property string _name: model.name
|
||||
readonly property string _icon: model.icon
|
||||
|
||||
width: root.width
|
||||
height: 34
|
||||
|
||||
ChannelItem {
|
||||
width: root.width
|
||||
channelId: _cid
|
||||
name: _name
|
||||
icon: _icon
|
||||
hasUnread: false
|
||||
isSelected: root.selectedChannel === _cid
|
||||
onClicked: root.selectedChannel = _cid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: Theme.sp24; width: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inline sub-components ─────────────────────────────────────────────────
|
||||
|
||||
// Section collapsible header
|
||||
component SectionHeader: Item {
|
||||
height: 32
|
||||
|
||||
required property string label
|
||||
required property bool isOpen
|
||||
signal toggle
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp8
|
||||
anchors.rightMargin: Theme.sp12
|
||||
spacing: Theme.sp4
|
||||
|
||||
// Chevron
|
||||
Text {
|
||||
text: isOpen ? "▾" : "▸"
|
||||
font.pixelSize: Theme.font10
|
||||
color: Theme.textMuted
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: label
|
||||
font.pixelSize: Theme.font11
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textMuted
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
|
||||
// Add-channel icon placeholder
|
||||
Text {
|
||||
text: "+"
|
||||
font.pixelSize: Theme.font16
|
||||
color: Theme.textMuted
|
||||
opacity: sectionHover.hovered ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: sectionHover }
|
||||
TapHandler { onTapped: parent.toggle() }
|
||||
}
|
||||
|
||||
// Individual channel row
|
||||
component ChannelItem: Item {
|
||||
id: chItem
|
||||
height: 34
|
||||
|
||||
required property int channelId
|
||||
required property string name
|
||||
required property string icon
|
||||
required property bool hasUnread
|
||||
required property bool isSelected
|
||||
signal clicked
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp8
|
||||
anchors.rightMargin: Theme.sp8
|
||||
radius: Theme.radius4
|
||||
color: chItem.isSelected ? Theme.accentSubtle
|
||||
: chHover.hovered ? Theme.bgHover
|
||||
: "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp16
|
||||
anchors.rightMargin: Theme.sp12
|
||||
spacing: Theme.sp6
|
||||
|
||||
Text {
|
||||
text: chItem.icon
|
||||
font.pixelSize: Theme.font14
|
||||
color: chItem.hasUnread ? Theme.textPrimary : Theme.textMuted
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: chItem.name
|
||||
font.pixelSize: Theme.font14
|
||||
font.weight: chItem.hasUnread ? Font.DemiBold : Font.Normal
|
||||
color: chItem.isSelected ? Theme.textPrimary
|
||||
: chItem.hasUnread ? Theme.textPrimary
|
||||
: chHover.hovered ? Theme.textSecondary
|
||||
: Theme.textMuted
|
||||
elide: Text.ElideRight
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
// Unread dot
|
||||
Rectangle {
|
||||
width: 6
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Theme.textPrimary
|
||||
visible: chItem.hasUnread && !chItem.isSelected
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: chHover }
|
||||
TapHandler { onTapped: chItem.clicked() }
|
||||
}
|
||||
}
|
||||
141
src/qml/components/ChatArea.qml
Normal file
141
src/qml/components/ChatArea.qml
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Main chat panel: header + message list + message input.
|
||||
Rectangle {
|
||||
id: root
|
||||
color: Theme.bgSurface
|
||||
|
||||
property string channelName: "general"
|
||||
property string channelTopic: "General discussion about Aether and Stoat"
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// ── Channel header ────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Constants.headerHeight
|
||||
color: Theme.bgSurface
|
||||
z: 1 // Render above message list so shadow shows
|
||||
|
||||
// Bottom shadow line
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: Theme.separator
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp16
|
||||
anchors.rightMargin: Theme.sp16
|
||||
spacing: Theme.sp12
|
||||
|
||||
// Channel icon + name
|
||||
Text {
|
||||
text: "#"
|
||||
font.pixelSize: Theme.font20
|
||||
font.weight: Font.Light
|
||||
color: Theme.textMuted
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.channelName
|
||||
font.pixelSize: Theme.font16
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textPrimary
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: Theme.font16
|
||||
color: Theme.border
|
||||
}
|
||||
|
||||
// Topic
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.channelTopic
|
||||
font.pixelSize: Theme.font13
|
||||
color: Theme.textMuted
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Header action icons
|
||||
HeaderButton { icon: "🔍"; tooltip: "Search" }
|
||||
HeaderButton { icon: "📌"; tooltip: "Pinned Messages" }
|
||||
HeaderButton { icon: "👥"; tooltip: "Member List" }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Message list ──────────────────────────────────────────────────────
|
||||
MessageList {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// ── Message input ─────────────────────────────────────────────────────
|
||||
MessageInput {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Constants.inputAreaHeight
|
||||
channelName: root.channelName
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inline header button component ────────────────────────────────────────
|
||||
component HeaderButton: Item {
|
||||
id: hBtn
|
||||
width: 32
|
||||
height: 32
|
||||
|
||||
property string icon
|
||||
property string tooltip
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.radius4
|
||||
color: hBtnHover.hovered ? Theme.bgHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: hBtn.icon
|
||||
font.pixelSize: Theme.font16
|
||||
color: hBtnHover.hovered ? Theme.textSecondary : Theme.textMuted
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
HoverHandler { id: hBtnHover }
|
||||
|
||||
ToolTip.visible: hBtnHover.hovered
|
||||
ToolTip.text: hBtn.tooltip
|
||||
ToolTip.delay: 400
|
||||
}
|
||||
}
|
||||
171
src/qml/components/GuildList.qml
Normal file
171
src/qml/components/GuildList.qml
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Vertical strip of guild/server icons on the far left.
|
||||
/// Each guild shows a circular icon with the guild initial letter.
|
||||
/// Hovering reveals a pill indicator; the selected guild shows a full pill.
|
||||
Rectangle {
|
||||
id: root
|
||||
color: Theme.bgBase
|
||||
|
||||
property int selectedIndex: 0
|
||||
|
||||
// ── Placeholder guild data ────────────────────────────────────────────────
|
||||
ListModel {
|
||||
id: guildsModel
|
||||
ListElement { guildName: "Home"; letter: "H"; iconColor: "#7c6af7" }
|
||||
ListElement { guildName: "Stoat HQ"; letter: "S"; iconColor: "#a6e3a1" }
|
||||
ListElement { guildName: "Gaming"; letter: "G"; iconColor: "#f38ba8" }
|
||||
ListElement { guildName: "Design"; letter: "D"; iconColor: "#fab387" }
|
||||
ListElement { guildName: "Dev Talk"; letter: "V"; iconColor: "#89dceb" }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Spacer at top
|
||||
Item { Layout.preferredHeight: Theme.sp8 }
|
||||
|
||||
// Home button separator after first item is handled visually via spacing
|
||||
|
||||
// Guild list
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: guildsModel
|
||||
clip: true
|
||||
spacing: Theme.sp4
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOff }
|
||||
|
||||
delegate: Item {
|
||||
id: guildItem
|
||||
width: root.width
|
||||
height: 64
|
||||
|
||||
readonly property bool isSelected: root.selectedIndex === model.index
|
||||
readonly property bool isHovered: hoverHandler.hovered
|
||||
|
||||
// ── Selection pill ────────────────────────────────────────────
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: 4
|
||||
height: guildItem.isSelected ? 40
|
||||
: guildItem.isHovered ? 20
|
||||
: 0
|
||||
radius: 2
|
||||
color: Theme.textPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.animNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Guild icon ────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
id: iconRect
|
||||
width: Constants.guildIconSize
|
||||
height: Constants.guildIconSize
|
||||
radius: guildItem.isSelected || guildItem.isHovered
|
||||
? Theme.radius16 : Constants.guildIconSize / 2
|
||||
color: model.iconColor
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation { duration: Theme.animNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.letter
|
||||
font.pixelSize: Theme.font18
|
||||
font.weight: Font.Bold
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: hoverHandler }
|
||||
TapHandler { onTapped: root.selectedIndex = model.index }
|
||||
|
||||
ToolTip.visible: hoverHandler.hovered
|
||||
ToolTip.text: model.guildName
|
||||
ToolTip.delay: 500
|
||||
}
|
||||
|
||||
// Separator after Home (index 0)
|
||||
footer: Item {
|
||||
width: root.width
|
||||
height: Theme.sp8
|
||||
|
||||
Rectangle {
|
||||
width: root.width * 0.6
|
||||
height: 2
|
||||
radius: 1
|
||||
color: Theme.border
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.sp4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add server button ─────────────────────────────────────────────────
|
||||
Item {
|
||||
Layout.preferredWidth: root.width
|
||||
Layout.preferredHeight: 64
|
||||
|
||||
Rectangle {
|
||||
id: addBtn
|
||||
width: Constants.guildIconSize
|
||||
height: Constants.guildIconSize
|
||||
radius: addHover.hovered ? Theme.radius16 : Constants.guildIconSize / 2
|
||||
color: addHover.hovered ? Theme.accent : Theme.bgMantle
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on radius { NumberAnimation { duration: Theme.animNormal; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: Theme.animNormal } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "+"
|
||||
font.pixelSize: Theme.font22
|
||||
font.weight: Font.Light
|
||||
color: addHover.hovered ? "#ffffff" : Theme.accent
|
||||
Behavior on color { ColorAnimation { duration: Theme.animNormal } }
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: addHover }
|
||||
|
||||
ToolTip.visible: addHover.hovered
|
||||
ToolTip.text: "Add a Server"
|
||||
ToolTip.delay: 500
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: Theme.sp8 }
|
||||
}
|
||||
}
|
||||
179
src/qml/components/MemberList.qml
Normal file
179
src/qml/components/MemberList.qml
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Right sidebar listing online/offline members.
|
||||
Rectangle {
|
||||
id: root
|
||||
color: Theme.bgMantle
|
||||
|
||||
// ── Placeholder member data ───────────────────────────────────────────────
|
||||
ListModel {
|
||||
id: membersModel
|
||||
|
||||
// ONLINE
|
||||
ListElement { username: "alice"; status: "online"; avatarColor: "#7c6af7"; role: "ONLINE" }
|
||||
ListElement { username: "bob"; status: "idle"; avatarColor: "#a6e3a1"; role: "ONLINE" }
|
||||
ListElement { username: "charlie"; status: "dnd"; avatarColor: "#f38ba8"; role: "ONLINE" }
|
||||
ListElement { username: "diana"; status: "online"; avatarColor: "#fab387"; role: "ONLINE" }
|
||||
ListElement { username: "eve"; status: "online"; avatarColor: "#89dceb"; role: "ONLINE" }
|
||||
// OFFLINE
|
||||
ListElement { username: "frank"; status: "offline"; avatarColor: "#585b70"; role: "OFFLINE" }
|
||||
ListElement { username: "grace"; status: "offline"; avatarColor: "#585b70"; role: "OFFLINE" }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Constants.headerHeight
|
||||
color: Theme.bgMantle
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: Theme.separator
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.sp16
|
||||
text: "MEMBERS"
|
||||
font.pixelSize: Theme.font11
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textMuted
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// Member list
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: memberListView
|
||||
model: membersModel
|
||||
topMargin: Theme.sp8
|
||||
|
||||
section.property: "role"
|
||||
section.delegate: Item {
|
||||
width: root.width
|
||||
height: 28
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.sp16
|
||||
text: section
|
||||
font.pixelSize: Theme.font11
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textMuted
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: memberItem
|
||||
width: root.width
|
||||
height: 44
|
||||
|
||||
readonly property color statusColor:
|
||||
model.status === "online" ? Theme.statusOnline :
|
||||
model.status === "idle" ? Theme.statusIdle :
|
||||
model.status === "dnd" ? Theme.statusDnd :
|
||||
Theme.statusOffline
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp8
|
||||
anchors.rightMargin: Theme.sp8
|
||||
radius: Theme.radius4
|
||||
color: memberHover.hovered ? Theme.bgHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp12
|
||||
anchors.rightMargin: Theme.sp12
|
||||
spacing: Theme.sp12
|
||||
|
||||
// Avatar + status dot
|
||||
Item {
|
||||
width: Constants.avatarSm
|
||||
height: Constants.avatarSm
|
||||
|
||||
Rectangle {
|
||||
width: Constants.avatarSm
|
||||
height: Constants.avatarSm
|
||||
radius: Constants.avatarSm / 2
|
||||
color: model.avatarColor
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.username.charAt(0).toUpperCase()
|
||||
font.pixelSize: Theme.font13
|
||||
font.weight: Font.Bold
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
// Status dot
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
color: memberItem.statusColor
|
||||
border.color: Theme.bgMantle
|
||||
border.width: 2
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: model.username
|
||||
font.pixelSize: Theme.font14
|
||||
color: memberHover.hovered
|
||||
? Theme.textPrimary : Theme.textSecondary
|
||||
elide: Text.ElideRight
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: memberHover }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/qml/components/MessageInput.qml
Normal file
141
src/qml/components/MessageInput.qml
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Message composition bar at the bottom of the chat area.
|
||||
Item {
|
||||
id: root
|
||||
height: Constants.inputAreaHeight
|
||||
|
||||
property string channelName: "general"
|
||||
signal messageSent(string text)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp16
|
||||
anchors.rightMargin: Theme.sp16
|
||||
anchors.topMargin: Theme.sp12
|
||||
anchors.bottomMargin: Theme.sp12
|
||||
radius: Theme.radius8
|
||||
color: Theme.bgOverlay
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp4
|
||||
anchors.rightMargin: Theme.sp4
|
||||
spacing: 0
|
||||
|
||||
// ── Attachment / add button ───────────────────────────────────────
|
||||
IconButton {
|
||||
text: "+"
|
||||
tooltip: "Upload File"
|
||||
}
|
||||
|
||||
// ── Text input ────────────────────────────────────────────────────
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
TextArea {
|
||||
id: textInput
|
||||
placeholderText: "Message #" + root.channelName
|
||||
placeholderTextColor: Theme.textMuted
|
||||
color: Theme.textPrimary
|
||||
font.pixelSize: Theme.font14
|
||||
background: null
|
||||
wrapMode: TextInput.Wrap
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
leftPadding: Theme.sp8
|
||||
rightPadding: Theme.sp8
|
||||
topPadding: Theme.sp12
|
||||
bottomPadding: Theme.sp12
|
||||
|
||||
Keys.onReturnPressed: (event) => {
|
||||
if (!(event.modifiers & Qt.ShiftModifier) && text.trim() !== "") {
|
||||
root.messageSent(text.trim())
|
||||
text = ""
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emoji button ──────────────────────────────────────────────────
|
||||
IconButton {
|
||||
text: "🙂"
|
||||
tooltip: "Emoji"
|
||||
}
|
||||
|
||||
// ── Send button ───────────────────────────────────────────────────
|
||||
IconButton {
|
||||
text: "➤"
|
||||
tooltip: "Send Message"
|
||||
enabled: textInput.text.trim() !== ""
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.animFast } }
|
||||
onClicked: {
|
||||
if (textInput.text.trim() !== "") {
|
||||
root.messageSent(textInput.text.trim())
|
||||
textInput.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inline icon button component ──────────────────────────────────────────
|
||||
component IconButton: Item {
|
||||
id: iconBtn
|
||||
width: 40
|
||||
height: 40
|
||||
|
||||
property string text
|
||||
property string tooltip
|
||||
signal clicked
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.sp4
|
||||
radius: Theme.radius4
|
||||
color: btnHover.hovered ? Theme.bgHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: iconBtn.text
|
||||
font.pixelSize: Theme.font16
|
||||
color: btnHover.hovered ? Theme.textSecondary : Theme.textMuted
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
HoverHandler { id: btnHover }
|
||||
TapHandler { onTapped: iconBtn.clicked() }
|
||||
|
||||
ToolTip.visible: btnHover.hovered
|
||||
ToolTip.text: iconBtn.tooltip
|
||||
ToolTip.delay: 400
|
||||
}
|
||||
}
|
||||
193
src/qml/components/MessageList.qml
Normal file
193
src/qml/components/MessageList.qml
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Scrollable list of chat messages.
|
||||
/// Groups consecutive messages from the same author (compact display).
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// ── Placeholder message data ──────────────────────────────────────────────
|
||||
ListModel {
|
||||
id: messagesModel
|
||||
ListElement {
|
||||
author: "alice"
|
||||
avatarColor: "#7c6af7"
|
||||
content: "Hey everyone! Welcome to the Aether client. 🎉"
|
||||
timestamp: "Today at 09:00"
|
||||
isFirst: true
|
||||
}
|
||||
ListElement {
|
||||
author: "bob"
|
||||
avatarColor: "#a6e3a1"
|
||||
content: "This looks amazing! Really clean interface."
|
||||
timestamp: "Today at 09:02"
|
||||
isFirst: true
|
||||
}
|
||||
ListElement {
|
||||
author: "bob"
|
||||
avatarColor: "#a6e3a1"
|
||||
content: "Can't wait to connect it to a real Stoat server."
|
||||
timestamp: "Today at 09:02"
|
||||
isFirst: false
|
||||
}
|
||||
ListElement {
|
||||
author: "charlie"
|
||||
avatarColor: "#f38ba8"
|
||||
content: "The dark theme is 🔥. Love the purple accent."
|
||||
timestamp: "Today at 09:05"
|
||||
isFirst: true
|
||||
}
|
||||
ListElement {
|
||||
author: "alice"
|
||||
avatarColor: "#7c6af7"
|
||||
content: "Thanks! Built with Qt 6 + QML + Material. Fully native."
|
||||
timestamp: "Today at 09:07"
|
||||
isFirst: true
|
||||
}
|
||||
ListElement {
|
||||
author: "alice"
|
||||
avatarColor: "#7c6af7"
|
||||
content: "AGPL-3.0 licensed, open source, and self-hostable."
|
||||
timestamp: "Today at 09:07"
|
||||
isFirst: false
|
||||
}
|
||||
ListElement {
|
||||
author: "diana"
|
||||
avatarColor: "#fab387"
|
||||
content: "This is exactly what the self-hosting community needed. 💜"
|
||||
timestamp: "Today at 09:10"
|
||||
isFirst: true
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
model: messagesModel
|
||||
clip: true
|
||||
topMargin: Theme.sp16
|
||||
bottomMargin: Theme.sp8
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
onCountChanged: Qt.callLater(() => listView.positionViewAtEnd())
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: msgItem
|
||||
width: listView.width
|
||||
height: msgLayout.implicitHeight + (model.isFirst ? Theme.sp16 : Theme.sp2)
|
||||
|
||||
readonly property bool isHovered: msgHover.hovered
|
||||
|
||||
// Hover highlight
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: msgItem.isHovered ? Theme.bgHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: msgLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: model.isFirst ? Theme.sp16 : Theme.sp2
|
||||
anchors.leftMargin: Theme.sp16
|
||||
anchors.rightMargin: Theme.sp16
|
||||
spacing: Theme.sp12
|
||||
|
||||
// ── Avatar (only on first message in a group) ─────────────────
|
||||
Item {
|
||||
width: Constants.avatarMd
|
||||
height: Constants.avatarMd
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Constants.avatarMd / 2
|
||||
color: model.avatarColor
|
||||
visible: model.isFirst
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.author.charAt(0).toUpperCase()
|
||||
font.pixelSize: Theme.font16
|
||||
font.weight: Font.Bold
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
// Hover timestamp for compact messages (no avatar)
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.timestamp.split(" ").slice(-1)[0] // HH:MM
|
||||
font.pixelSize: Theme.font10
|
||||
color: Theme.textMuted
|
||||
visible: !model.isFirst && msgItem.isHovered
|
||||
}
|
||||
}
|
||||
|
||||
// ── Message body ──────────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.sp2
|
||||
|
||||
// Author + timestamp (first message only)
|
||||
RowLayout {
|
||||
visible: model.isFirst
|
||||
spacing: Theme.sp8
|
||||
|
||||
Text {
|
||||
text: model.author
|
||||
font.pixelSize: Theme.font14
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textPrimary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.timestamp
|
||||
font.pixelSize: Theme.font11
|
||||
color: Theme.textMuted
|
||||
}
|
||||
}
|
||||
|
||||
// Message content
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: model.content
|
||||
font.pixelSize: Theme.font14
|
||||
color: Theme.textSecondary
|
||||
wrapMode: Text.Wrap
|
||||
lineHeight: 1.375
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: msgHover }
|
||||
}
|
||||
}
|
||||
}
|
||||
157
src/qml/components/VoiceBar.qml
Normal file
157
src/qml/components/VoiceBar.qml
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
/// Bottom bar of the channel list column.
|
||||
/// Shows current voice connection status and quick-action buttons.
|
||||
Rectangle {
|
||||
id: root
|
||||
color: Theme.bgBase
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
property bool inVoice: true
|
||||
property bool muted: false
|
||||
property bool deafened: false
|
||||
property string channelName: "General"
|
||||
property string username: "me"
|
||||
|
||||
// Top separator
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: Theme.separator
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.sp8
|
||||
anchors.rightMargin: Theme.sp8
|
||||
spacing: Theme.sp4
|
||||
|
||||
// ── User avatar + name + status ───────────────────────────────────────
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Theme.accent
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.username.charAt(0).toUpperCase()
|
||||
font.pixelSize: Theme.font13
|
||||
font.weight: Font.Bold
|
||||
color: "#ffffff"
|
||||
}
|
||||
|
||||
// Online status dot
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
color: Theme.statusOnline
|
||||
border.color: Theme.bgBase
|
||||
border.width: 2
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
// Name + voice channel label
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: root.username
|
||||
font.pixelSize: Theme.font13
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.textPrimary
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.inVoice
|
||||
text: "🔊 " + root.channelName
|
||||
font.pixelSize: Theme.font11
|
||||
color: Theme.statusOnline
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mute button ───────────────────────────────────────────────────────
|
||||
VoiceButton {
|
||||
icon: root.muted ? "🚫" : "🎤"
|
||||
active: !root.muted
|
||||
tooltip: root.muted ? "Unmute" : "Mute"
|
||||
onClicked: root.muted = !root.muted
|
||||
}
|
||||
|
||||
// ── Deafen button ─────────────────────────────────────────────────────
|
||||
VoiceButton {
|
||||
icon: root.deafened ? "🔕" : "🎧"
|
||||
active: !root.deafened
|
||||
tooltip: root.deafened ? "Undeafen" : "Deafen"
|
||||
onClicked: root.deafened = !root.deafened
|
||||
}
|
||||
|
||||
// ── Settings button ───────────────────────────────────────────────────
|
||||
VoiceButton {
|
||||
icon: "⚙"
|
||||
active: true
|
||||
tooltip: "User Settings"
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inline button component ───────────────────────────────────────────────
|
||||
component VoiceButton: Rectangle {
|
||||
id: vBtn
|
||||
width: 28
|
||||
height: 28
|
||||
radius: Theme.radius4
|
||||
|
||||
required property string icon
|
||||
required property bool active
|
||||
required property string tooltip
|
||||
|
||||
signal clicked
|
||||
|
||||
color: btnHover.hovered ? Theme.bgHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: vBtn.icon
|
||||
font.pixelSize: Theme.font14
|
||||
color: vBtn.active ? Theme.textSecondary : Theme.statusDnd
|
||||
}
|
||||
|
||||
HoverHandler { id: btnHover }
|
||||
TapHandler { onTapped: vBtn.clicked() }
|
||||
|
||||
ToolTip.visible: btnHover.hovered
|
||||
ToolTip.text: vBtn.tooltip
|
||||
ToolTip.delay: 400
|
||||
}
|
||||
}
|
||||
|
|
@ -16,27 +16,11 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Aether 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
title: qsTr("Aether")
|
||||
width: 1280
|
||||
height: 800
|
||||
minimumWidth: 1280
|
||||
minimumHeight: 800
|
||||
visible: true
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.DeepPurple
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Aether is loading…")
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 24
|
||||
font.family: "Segoe UI, SF Pro Display, Inter, sans-serif"
|
||||
}
|
||||
}
|
||||
// Entry point — instantiates the application's main window.
|
||||
MainWindow {}
|
||||
|
|
|
|||
87
src/qml/styles/Theme.qml
Normal file
87
src/qml/styles/Theme.qml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pragma Singleton
|
||||
import QtQuick 2.15
|
||||
|
||||
/// Central design-token store for the Aether UI.
|
||||
/// Usage: Theme.bgBase, Theme.accent, Theme.font14, etc.
|
||||
QtObject {
|
||||
|
||||
// ── Backgrounds ───────────────────────────────────────────────────────────
|
||||
readonly property color bgBase: "#181825" // Guild strip (darkest)
|
||||
readonly property color bgMantle: "#1e1e2e" // Channel list / panels
|
||||
readonly property color bgSurface: "#313244" // Chat area
|
||||
readonly property color bgOverlay: "#45475a" // Input fields, elevated cards
|
||||
readonly property color bgHover: "#2d2d44" // Generic hover
|
||||
|
||||
// ── Accent (deep purple) ──────────────────────────────────────────────────
|
||||
readonly property color accent: "#7c6af7"
|
||||
readonly property color accentHover: "#9d8fff"
|
||||
readonly property color accentPressed: "#6355d6"
|
||||
readonly property color accentSubtle: "#33285c" // Selected row tint
|
||||
|
||||
// ── Text ──────────────────────────────────────────────────────────────────
|
||||
readonly property color textPrimary: "#cdd6f4"
|
||||
readonly property color textSecondary: "#a6adc8"
|
||||
readonly property color textMuted: "#6c7086"
|
||||
readonly property color textHeader: "#bac2de"
|
||||
readonly property color textLink: "#89b4fa"
|
||||
|
||||
// ── Status ────────────────────────────────────────────────────────────────
|
||||
readonly property color statusOnline: "#a6e3a1"
|
||||
readonly property color statusIdle: "#f9e2af"
|
||||
readonly property color statusDnd: "#f38ba8"
|
||||
readonly property color statusOffline: "#585b70"
|
||||
|
||||
// ── Borders / separators ──────────────────────────────────────────────────
|
||||
readonly property color border: "#313244"
|
||||
readonly property color separator: "#1e1e2e"
|
||||
|
||||
// ── Spacing (px) ──────────────────────────────────────────────────────────
|
||||
readonly property int sp2: 2
|
||||
readonly property int sp4: 4
|
||||
readonly property int sp6: 6
|
||||
readonly property int sp8: 8
|
||||
readonly property int sp12: 12
|
||||
readonly property int sp16: 16
|
||||
readonly property int sp20: 20
|
||||
readonly property int sp24: 24
|
||||
|
||||
// ── Border radii ──────────────────────────────────────────────────────────
|
||||
readonly property int radius4: 4
|
||||
readonly property int radius8: 8
|
||||
readonly property int radius12: 12
|
||||
readonly property int radius16: 16
|
||||
|
||||
// ── Font sizes (px) ───────────────────────────────────────────────────────
|
||||
readonly property int font10: 10
|
||||
readonly property int font11: 11
|
||||
readonly property int font12: 12
|
||||
readonly property int font13: 13
|
||||
readonly property int font14: 14
|
||||
readonly property int font16: 16
|
||||
readonly property int font18: 18
|
||||
readonly property int font20: 20
|
||||
readonly property int font22: 22
|
||||
|
||||
// ── Transition durations (ms) ─────────────────────────────────────────────
|
||||
readonly property int animFast: 100
|
||||
readonly property int animNormal: 180
|
||||
readonly property int animSlow: 300
|
||||
}
|
||||
45
src/qml/utils/Constants.qml
Normal file
45
src/qml/utils/Constants.qml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Aether Contributors
|
||||
//
|
||||
// This file is part of Aether.
|
||||
//
|
||||
// Aether is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Aether is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
// License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with Aether. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pragma Singleton
|
||||
import QtQuick 2.15
|
||||
|
||||
/// App-wide layout dimensions and configuration defaults.
|
||||
/// Usage: Constants.guildListWidth, Constants.appName, etc.
|
||||
QtObject {
|
||||
|
||||
readonly property string appName: "Aether"
|
||||
readonly property string appVersion: "0.1.0"
|
||||
|
||||
// ── Fixed layout dimensions ───────────────────────────────────────────────
|
||||
readonly property int guildListWidth: 72
|
||||
readonly property int channelListWidth: 240
|
||||
readonly property int memberListWidth: 240
|
||||
readonly property int voiceBarHeight: 52
|
||||
readonly property int headerHeight: 48
|
||||
readonly property int inputAreaHeight: 72
|
||||
|
||||
// ── Window constraints ────────────────────────────────────────────────────
|
||||
readonly property int minWindowWidth: 1280
|
||||
readonly property int minWindowHeight: 800
|
||||
|
||||
// ── Avatar sizes ──────────────────────────────────────────────────────────
|
||||
readonly property int avatarMd: 40
|
||||
readonly property int avatarSm: 32
|
||||
readonly property int guildIconSize: 48
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue