-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): add event card, card group and conversion function
- Loading branch information
1 parent
ce2cdeb
commit 47eaab7
Showing
7 changed files
with
422 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
#pragma once | ||
|
||
#include <Infrastructure/Network/ResponseStruct.h> | ||
#include <app.h> | ||
#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 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.