Skip to content

Commit

Permalink
feat(ui): add event card, card group and conversion function
Browse files Browse the repository at this point in the history
  • Loading branch information
Mairon1206 committed Aug 26, 2024
1 parent ce2cdeb commit 47eaab7
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 1 deletion.
124 changes: 124 additions & 0 deletions src/Controller/Convert.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#pragma once

#include <Infrastructure/Network/ResponseStruct.h>
#include <app.h>

Check failure on line 4 in src/Controller/Convert.hh

View workflow job for this annotation

GitHub Actions / review

'app.h' file not found [clang-diagnostic-error]
#include <memory>
#include <spdlog/spdlog.h>
#include <string>
#include <vector>

namespace evento::convert {

namespace details {

slint::SharedString convertTimeRange(const std::string& startTimeStr,
const std::string& endTimeStr) {
std::istringstream ssStart(startTimeStr);
std::istringstream ssEnd(endTimeStr);
std::chrono::sys_seconds startTp, endTp;

ssStart >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", startTp);
if (ssStart.fail()) {
spdlog::warn("Failed to parse start-time string: {}", startTimeStr);
return " ";
}
ssEnd >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", endTp);
if (ssEnd.fail()) {
spdlog::warn("Failed to parse end-time string: {}", endTimeStr);
return " ";
}

auto startTimer = std::chrono::system_clock::to_time_t(startTp);
auto startDate = *std::gmtime(&startTimer);

auto endTimer = std::chrono::system_clock::to_time_t(endTp);
auto endDate = *std::gmtime(&endTimer);

std::string startStr = std::format("{:02}.{:02} {:02}:{:02}",
startDate.tm_mon + 1,
startDate.tm_mday,
startDate.tm_hour,
startDate.tm_min);
std::string endStr = std::format("{:02}.{:02} {:02}:{:02}",
endDate.tm_mon + 1,
endDate.tm_mday,
endDate.tm_hour,
endDate.tm_min);

if (startDate.tm_year != endDate.tm_year) {
return slint::SharedString{std::format("{:04} {} - {:04} {}",
startDate.tm_year + 1900,
startStr,
endDate.tm_year + 1900,
endStr)};
}
if (startDate.tm_mon != endDate.tm_mon || startDate.tm_mday != endDate.tm_mday) {
return slint::SharedString{startStr + " - " + endStr};
}
return slint::SharedString{startStr + " - " + endStr.substr(6)};
}

slint::SharedString firstUnicode(const std::string& str) {
if (str.empty()) {
return " ";
}
size_t length = 0;
auto firstByte = static_cast<unsigned char>(str[0]);
if ((firstByte & 0x80) == 0) {
// ASCII character
length = 1;
} else if ((firstByte & 0xE0) == 0xC0) {
// Two-byte character
length = 2;
} else if ((firstByte & 0xF0) == 0xE0) {
// Three-byte character
length = 3;
} else if ((firstByte & 0xF8) == 0xF0) {
// Four-byte character
length = 4;
} else {
// Invalid Unicode
return " ";
}
if (str.size() < length) {
return " ";
}
return slint::SharedString{str.substr(0, length)};
}

} // namespace details

using EventEntityList = std::vector<EventEntity>;
using EventStructModel = std::shared_ptr<slint::VectorModel<EventStruct>>;

auto from(const auto& obj) {
return obj;
}

EventStruct from(const EventEntity& entity) {
return {
.id = entity.id,
.summary = slint::SharedString(entity.summary),
.summary_abbr = details::firstUnicode(entity.summary),
.description = slint::SharedString(entity.description),
.time = details::convertTimeRange(entity.start, entity.end),
.location = slint::SharedString(entity.location),
.tag = slint::SharedString(entity.tag),
.larkMeetingRoomName = slint::SharedString(entity.larkMeetingRoomName),
.larkDepartmentName = slint::SharedString(entity.larkDepartmentName),
.state = static_cast<EventState>(entity.state),
.is_subscribed = entity.isSubscribed,
.is_checkIn = entity.isCheckedIn,
};
}

EventStructModel from(const EventEntityList& list) {
std::vector<EventStruct> model;
model.reserve(list.size());
for (auto& entity : list) {
model.push_back(from(entity));
}
return std::make_shared<slint::VectorModel<EventStruct>>(model);
}

} // namespace evento::convert
1 change: 1 addition & 0 deletions ui/assets/image/icon/pentagon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions ui/assets/image/image_token.slint
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct EventoIconCollection {
topic: image,
tree-list: image,
account-box: image,
pentagon: image,
}

struct EventoDisplayCollection {
Expand Down Expand Up @@ -94,6 +95,7 @@ export global EventoImageToken {
topic: @image-url("./icon/topic.svg"),
tree-list: @image-url("./icon/tree-list.svg"),
account-box: @image-url("./icon/account-box-outline.svg"),
pentagon: @image-url("./icon/pentagon.svg"),
};
// display used as images bigger than icon
// most display image won't change according to darkmode switch
Expand Down
227 changes: 227 additions & 0 deletions ui/components/event_card.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { Token } from "../global.slint";
import { EventStruct } from "./event_struct.slint";

export component EventCard inherits Rectangle {
property <color> surface: Token.color.surface-container-low;
property <color> on-surface: Token.color.on-surface-variant;
property <color> inverse-surface: Token.color.inverse-surface;
property <color> inverse-on-surface: Token.color.inverse-on-surface;
property <length> vertical-padding: 14px;
property <length> horizontal-padding: 14px;
property <length> press-x;
property <length> press-y;
callback clicked;
in-out property <EventStruct> event: {
summary: "活动标题",
summary-abbr: "活",
time: "活动时间",
location: "活动地点",
description: "活动内容",
id: 0
};
width: 303px;
height: 188px;
clip: true;
background: surface;
border-radius: 12px;

states [
ripple when touch-area.pressed: {
drop-shadow-color: Token.color.surface;
drop-shadow-blur: 0px;
drop-shadow-offset-y: 0px;
animate-circle.radius: root.width * 1.1;
in {
animate drop-shadow-color, drop-shadow-blur, drop-shadow-offset-y, animate-circle.radius { duration: 200ms; }
}
out {
animate drop-shadow-color, drop-shadow-blur, drop-shadow-offset-y, animate-circle.radius { duration: 200ms; }
}
}
origin when !touch-area.pressed: {
drop-shadow-color: touch-area.has-hover ? #070707ce : Token.color.surface;
drop-shadow-blur: touch-area.has-hover ? 4px : 0px;
drop-shadow-offset-y: touch-area.has-hover ? 2px : 0px;
animate-circle.radius: 0px;
}
]

touch-area := TouchArea {
width: 100%;
height: 100%;
clicked => {
root.clicked();
}
}

animate-circle := Rectangle {
in-out property <length> radius;
in-out property <length> center-x;
in-out property <length> center-y;
center-x: touch-area.pressed-x;
center-y: touch-area.pressed-y;
radius: 0px;
x: center-x - radius;
y: center-y - radius;
width: radius * 2;
height: radius * 2;
border-radius: radius;
background: Token.color.surface-variant;
}

abbr-circle := Rectangle {
x: horizontal-padding;
y: vertical-padding;
width: 28px;
height: self.width;
border-radius: self.width / 2;
background: on-surface;
abbr-text := Text {
color: inverse-on-surface;
text: event.summary-abbr;
font-size: Token.font.label.small.size;
font-weight: Token.font.label.small.weight;
}
}

pentagon := Image {
x: parent.width - self.width - horizontal-padding;
y: abbr-circle.y + (abbr-circle.height - self.height) / 2;
width: 10px;
height: self.width;
colorize: on-surface;
source: Token.image.icon.pentagon;
}

title-layout := VerticalLayout {
x: horizontal-padding;
y: abbr-circle.y + abbr-circle.height;
width: root.width - horizontal-padding * 2;
height: info-layout.y - self.y;
alignment: center;
event-title := Text {
color: inverse-surface;
text: event.summary;
wrap: word-wrap;
width: parent.width;
font-size: Token.font.body.large.size;
font-weight: Token.font.headline.large.weight;
}
}

info-layout := VerticalLayout {
x: horizontal-padding;
y: root.height - self.height - vertical-padding;
width: root.width - horizontal-padding * 2;
height: time-label.height * 3 + self.spacing * 2;
alignment: center;
spacing: 2px;
time-label := Rectangle {
width: root.width - root.horizontal-padding * 2;
height: 20px;
HorizontalLayout {
spacing: 6px;
time-icon := Image {
width: 16px;
height: self.width;
source: Token.image.icon.time;
colorize: time-text.color;
}

time-text := Text {
font-size: Token.font.label.small.size;
text: event.time;
width: parent.width - time-icon.width - parent.spacing;
overflow: elide;
}
}
}

location-label := Rectangle {
width: root.width - root.horizontal-padding * 2;
height: 20px;
HorizontalLayout {
spacing: 6px;
location-icon := Image {
width: 16px;
height: self.width;
source: Token.image.icon.locate;
colorize: location-text.color;
}

location-text := Text {
font-size: Token.font.label.small.size;
text: event.location;
width: parent.width - location-icon.width - parent.spacing;
overflow: elide;
}
}
}

description-label := Rectangle {
width: root.width - root.horizontal-padding * 2;
height: 20px;
HorizontalLayout {
spacing: 6px;
description-icon := Image {
width: 16px;
height: self.width;
source: Token.image.icon.topic;
colorize: location-text.color;
}

description-text := Text {
font-size: Token.font.label.small.size;
text: event.description;
width: parent.width - description-icon.width - parent.spacing;
overflow: elide;
}
}
}
}
}

export component EventCardGroup inherits Rectangle {
in-out property <[EventStruct]> events;
in-out property <length> horizontal-spacing: 20px;
in-out property <length> vertical-spacing: 20px;
in-out property <length> card-width: 236px;
out property <length> card-height: 188px;
property <int> row-count: floor(events.length / 3) * 3 == events.length ? events.length / 3 : floor(events.length / 3) + 1;
callback card-clicked(EventStruct);
width: card-width * 3 + horizontal-spacing * 4;
height: card-height * 3 + vertical-spacing * (row-count + 1);

function row-indexes(start-idx: int) -> [int] {
if start-idx + 1 < root.events.length {
if start-idx + 2 < root.events.length {
return [start-idx, start-idx + 1, start-idx + 2];
} else {
return [start-idx, start-idx + 1];
}
}
return [start-idx];
}

VerticalLayout {
width: 100%;
height: 100%;
alignment: center;
spacing: root.vertical-spacing;
for row-idx in row-count: HorizontalLayout {
property <int> start-idx: row-idx * 3;
width: 100%;
height: root.card-height;
alignment: start;
padding: (self.width - self.spacing * 2 - root.card-width * 3) / 2;
spacing: root.horizontal-spacing;
for idx in root.row-indexes(start-idx): EventCard {
event: root.events[idx];
width: root.card-width;
clicked => {
root.card-clicked(self.event);
}
}
}
}
}
21 changes: 21 additions & 0 deletions ui/components/event_struct.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export enum EventState {
SigningUp,
Active,
Completed,
Cancelled,
}

export struct EventStruct {
id: int,
summary: string,
summary-abbr: string,
description: string,
time: string,
location: string,
tag: string,
larkMeetingRoomName: string,
larkDepartmentName: string,
state: EventState,
is-subscribed: bool,
is-checkIn: bool
}
Loading

0 comments on commit 47eaab7

Please sign in to comment.