mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 13:06:45 +08:00
[feat] add DeskHub device wall and shared group actions
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GALLERY_FULLSCREEN_TRANSITION_H_
|
||||
#define _GALLERY_FULLSCREEN_TRANSITION_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk::gallery {
|
||||
|
||||
// Driven by the existing UI tick; never pumps events or creates nested timers.
|
||||
class FullscreenTransition {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
enum class Action { Idle, Apply, Reveal, Finished };
|
||||
|
||||
bool Begin(bool fullscreen, Clock::time_point now) {
|
||||
if (Active())
|
||||
return false;
|
||||
target_ = fullscreen;
|
||||
phase_ = Phase::FadingOut;
|
||||
phase_at_ = now;
|
||||
width_ = height_ = 0;
|
||||
scale_ = 0;
|
||||
geometry_changed_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Action Advance(Clock::time_point now, uint32_t width, uint32_t height,
|
||||
float scale, bool fullscreen, bool restoring) {
|
||||
using namespace std::chrono_literals;
|
||||
if (phase_ == Phase::FadingOut) {
|
||||
if (restoring && now - phase_at_ >= 1500ms) {
|
||||
Reveal(now);
|
||||
return Action::Reveal;
|
||||
}
|
||||
// Slint fades out for 60ms; allow another frame before native resizing.
|
||||
if (now - phase_at_ < 80ms || restoring)
|
||||
return Action::Idle;
|
||||
phase_ = Phase::Settling;
|
||||
phase_at_ = stable_at_ = now;
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
scale_ = scale;
|
||||
return Action::Apply;
|
||||
}
|
||||
if (phase_ == Phase::Settling) {
|
||||
const bool changed =
|
||||
width != width_ || height != height_ || scale != scale_;
|
||||
if (changed && width > 0 && height > 0)
|
||||
geometry_changed_ = true;
|
||||
if (changed || fullscreen != target_ || restoring) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
scale_ = scale;
|
||||
stable_at_ = now;
|
||||
}
|
||||
// Some backends report the requested flag before delivering new bounds.
|
||||
// Also allow fullscreen modes whose client dimensions genuinely match.
|
||||
if ((width > 0 && height > 0 && fullscreen == target_ && !restoring &&
|
||||
(geometry_changed_ || now - phase_at_ >= 300ms) &&
|
||||
now - stable_at_ >= 32ms) ||
|
||||
now - phase_at_ >= 1500ms) {
|
||||
Reveal(now);
|
||||
return Action::Reveal;
|
||||
}
|
||||
}
|
||||
if (phase_ == Phase::FadingIn && now - phase_at_ >= 80ms) {
|
||||
Cancel();
|
||||
return Action::Finished;
|
||||
}
|
||||
return Action::Idle;
|
||||
}
|
||||
|
||||
void Reveal(Clock::time_point now) {
|
||||
phase_ = Phase::FadingIn;
|
||||
phase_at_ = now;
|
||||
}
|
||||
void Cancel() { phase_ = Phase::Idle; }
|
||||
bool Active() const { return phase_ != Phase::Idle; }
|
||||
bool Target() const { return target_; }
|
||||
bool ContentVisible() const {
|
||||
return phase_ == Phase::Idle || phase_ == Phase::FadingIn;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Phase { Idle, FadingOut, Settling, FadingIn };
|
||||
Phase phase_ = Phase::Idle;
|
||||
bool target_ = false;
|
||||
bool geometry_changed_ = false;
|
||||
uint32_t width_ = 0, height_ = 0;
|
||||
float scale_ = 0;
|
||||
Clock::time_point phase_at_{}, stable_at_{};
|
||||
};
|
||||
|
||||
} // namespace crossdesk::gallery
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "gallery_storage.h"
|
||||
|
||||
#include <fstream>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
namespace crossdesk::gallery {
|
||||
bool WriteAtomically(const std::filesystem::path &path,
|
||||
const std::string &text) {
|
||||
auto temporary = path;
|
||||
temporary += ".tmp";
|
||||
try {
|
||||
{
|
||||
std::ofstream output;
|
||||
output.exceptions(std::ios::failbit | std::ios::badbit);
|
||||
output.open(temporary, std::ios::binary | std::ios::trunc);
|
||||
output.write(text.data(), text.size());
|
||||
output.close();
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExW(temporary.c_str(), path.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||
return true;
|
||||
#else
|
||||
std::filesystem::rename(temporary, path);
|
||||
return true;
|
||||
#endif
|
||||
} catch (...) {
|
||||
}
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
return false;
|
||||
}
|
||||
} // namespace crossdesk::gallery
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GALLERY_STORAGE_H_
|
||||
#define _GALLERY_STORAGE_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk::gallery {
|
||||
bool WriteAtomically(const std::filesystem::path &path,
|
||||
const std::string &text);
|
||||
} // namespace crossdesk::gallery
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GALLERY_WINDOW_STATE_H_
|
||||
#define _GALLERY_WINDOW_STATE_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
|
||||
namespace crossdesk::gallery {
|
||||
|
||||
struct WindowState {
|
||||
bool wall_fullscreen = false;
|
||||
bool wall_maximized = false;
|
||||
// Store client dimensions in logical pixels, independent of the display DPI.
|
||||
std::optional<slint::LogicalSize> wall_size;
|
||||
std::optional<slint::PhysicalPosition> wall_position, connection_position;
|
||||
|
||||
void BeginModeChange(bool wall, slint::PhysicalPosition position,
|
||||
slint::PhysicalSize size, float scale, bool fullscreen,
|
||||
bool maximized, float compact_height = 486) {
|
||||
// Never capture a half-applied mode transition as the user's wall geometry.
|
||||
Observe(!wall, position, size, scale, fullscreen, maximized,
|
||||
compact_height);
|
||||
if (wall) {
|
||||
// Modes share the current desktop location. An old wall position must
|
||||
// not undo a move made while the connection page was visible.
|
||||
if (connection_position)
|
||||
wall_position = connection_position;
|
||||
} else {
|
||||
// Fullscreen/maximized coordinates describe the screen, not the ordinary
|
||||
// window. Return to the last normal anchor before restoring compact size.
|
||||
if (wall_position)
|
||||
connection_position = wall_position;
|
||||
}
|
||||
BeginRestore();
|
||||
}
|
||||
|
||||
void Observe(bool wall, slint::PhysicalPosition position,
|
||||
slint::PhysicalSize size, float scale, bool fullscreen,
|
||||
bool maximized, float compact_height = 486) {
|
||||
if (Restoring())
|
||||
return;
|
||||
if (!wall) {
|
||||
connection_position = position;
|
||||
return;
|
||||
}
|
||||
if (!fullscreen && !maximized && IsUndersized(size, scale, compact_height))
|
||||
return;
|
||||
scale = std::max(scale, 1.0f);
|
||||
wall_fullscreen = fullscreen;
|
||||
wall_maximized = !fullscreen && maximized;
|
||||
if (!fullscreen && !maximized) {
|
||||
wall_size = slint::LogicalSize(
|
||||
slint::Size<float>{size.width / scale, size.height / scale});
|
||||
wall_position = position;
|
||||
}
|
||||
}
|
||||
|
||||
bool RecoverUndersized(bool wall, slint::PhysicalPosition position,
|
||||
slint::PhysicalSize size, float scale, bool visible,
|
||||
bool minimized, bool fullscreen, bool maximized,
|
||||
float compact_height = 486) {
|
||||
// Hidden/minimized/uninitialized windows can report temporary tiny bounds.
|
||||
// Leave those alone, and let an in-flight restoration retain its target.
|
||||
if (Restoring() || !visible || minimized || fullscreen || maximized ||
|
||||
size.width == 0 || size.height == 0 ||
|
||||
!IsUndersized(size, scale, compact_height))
|
||||
return false;
|
||||
if (wall) {
|
||||
wall_position = position;
|
||||
wall_fullscreen = false;
|
||||
wall_maximized = false;
|
||||
} else {
|
||||
connection_position = position;
|
||||
}
|
||||
// Keep the last valid logical size; only the current anchor is captured.
|
||||
BeginRestore();
|
||||
return true;
|
||||
}
|
||||
|
||||
slint::PhysicalSize RestoreSize(bool wall, float scale,
|
||||
float compact_height = 486) const {
|
||||
const float width =
|
||||
wall && wall_size ? std::max(640.0f, wall_size->width) : 640;
|
||||
const float height = wall && wall_size
|
||||
? std::max(compact_height, wall_size->height)
|
||||
: compact_height;
|
||||
scale = std::max(scale, 1.0f);
|
||||
return slint::PhysicalSize(
|
||||
slint::Size<uint32_t>{uint32_t(std::lround(width * scale)),
|
||||
uint32_t(std::lround(height * scale))});
|
||||
}
|
||||
|
||||
void BeginRestore() {
|
||||
phase_ = Phase::Normal;
|
||||
target_.reset();
|
||||
last_size_.reset();
|
||||
matches_ = 0;
|
||||
}
|
||||
|
||||
void PrepareForShow(slint::Window &window, bool wall,
|
||||
slint::PhysicalSize size,
|
||||
std::optional<slint::PhysicalPosition> position,
|
||||
float initial_y_offset = 0) {
|
||||
const float scale = std::max(window.scale_factor(), 1.0f);
|
||||
// The actual display scale is resolved when the native window is created.
|
||||
// Give the backend logical geometry so its first visible frame is correct.
|
||||
window.set_size(slint::LogicalSize(
|
||||
slint::Size<float>{size.width / scale, size.height / scale}));
|
||||
if (position) {
|
||||
(wall ? wall_position : connection_position) = position;
|
||||
window.set_position(slint::LogicalPosition(slint::Point<float>{
|
||||
position->x / scale, position->y / scale + initial_y_offset}));
|
||||
}
|
||||
}
|
||||
bool Restoring() const { return phase_ != Phase::Idle; }
|
||||
bool RestoringNormal() const { return phase_ == Phase::Normal; }
|
||||
|
||||
// A set_size request can complete asynchronously, after layout constraints
|
||||
// and the backing scale have changed. Acknowledge observations on later
|
||||
// ticks.
|
||||
bool NormalSizeRestored(slint::PhysicalSize actual,
|
||||
slint::PhysicalSize target) {
|
||||
if (!RestoringNormal())
|
||||
return false;
|
||||
if (target_ && SameSize(*target_, target) && SameSize(actual, target))
|
||||
++matches_;
|
||||
else
|
||||
matches_ = 0;
|
||||
target_ = target;
|
||||
if (matches_ < 2)
|
||||
return false;
|
||||
phase_ = Phase::Presentation;
|
||||
matches_ = 0;
|
||||
last_size_.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FinishRestore(bool wall, slint::PhysicalSize actual, bool fullscreen,
|
||||
bool maximized) {
|
||||
if (phase_ != Phase::Presentation)
|
||||
return false;
|
||||
const bool want_fullscreen = wall && wall_fullscreen;
|
||||
const bool want_maximized = wall && !want_fullscreen && wall_maximized;
|
||||
const bool normal = !want_fullscreen && !want_maximized;
|
||||
if (normal && !fullscreen && !maximized && target_ &&
|
||||
!SameSize(actual, *target_)) {
|
||||
// A late constraint/DPI update can invalidate an earlier acknowledgement.
|
||||
// Reissue the normal size instead of getting stuck or saving that size.
|
||||
BeginRestore();
|
||||
return false;
|
||||
}
|
||||
const bool ready = fullscreen == want_fullscreen &&
|
||||
(want_fullscreen || maximized == want_maximized) &&
|
||||
(!normal || (target_ && SameSize(actual, *target_)));
|
||||
if (ready && last_size_ && SameSize(actual, *last_size_))
|
||||
++matches_;
|
||||
else
|
||||
matches_ = 0;
|
||||
last_size_ = actual;
|
||||
if (matches_ < 2)
|
||||
return false;
|
||||
phase_ = Phase::Idle;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Phase { Idle, Normal, Presentation };
|
||||
Phase phase_ = Phase::Idle;
|
||||
std::optional<slint::PhysicalSize> target_, last_size_;
|
||||
int matches_ = 0;
|
||||
static bool IsUndersized(slint::PhysicalSize size, float scale,
|
||||
float compact_height) {
|
||||
scale = std::max(scale, 1.0f);
|
||||
return double(size.width) + 1 < 640 * scale ||
|
||||
double(size.height) + 1 < compact_height * scale;
|
||||
}
|
||||
static bool SameSize(slint::PhysicalSize a, slint::PhysicalSize b) {
|
||||
return std::abs(double(a.width) - b.width) <= 1 &&
|
||||
std::abs(double(a.height) - b.height) <= 1;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace crossdesk::gallery
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GALLERY_WORKSPACE_H_
|
||||
#define _GALLERY_WORKSPACE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gallery_storage.h"
|
||||
|
||||
namespace crossdesk::gallery {
|
||||
struct Member {
|
||||
std::string id;
|
||||
std::string group;
|
||||
};
|
||||
|
||||
// Layout contains no credentials and never reconnects devices when loaded.
|
||||
struct Workspace {
|
||||
// Group assignments are shared by history and the wall. Wall enrollment is
|
||||
// independent, so assigning a group alone must never add a device to it.
|
||||
std::vector<Member> members;
|
||||
std::vector<std::string> groups;
|
||||
std::set<std::string> selected;
|
||||
int page = 0;
|
||||
int capacity = 6;
|
||||
|
||||
static std::string NormalizeId(const std::string &input) {
|
||||
std::string result;
|
||||
for (size_t i = 0; i < input.size();) {
|
||||
const auto ch = static_cast<unsigned char>(input[i]);
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (i + 2 < input.size() && ch == 0xEF &&
|
||||
static_cast<unsigned char>(input[i + 1]) == 0xBC &&
|
||||
static_cast<unsigned char>(input[i + 2]) >= 0x90 &&
|
||||
static_cast<unsigned char>(input[i + 2]) <= 0x99) {
|
||||
result += char('0' + static_cast<unsigned char>(input[i + 2]) - 0x90);
|
||||
i += 3;
|
||||
} else if (input.compare(i, 3, "\xe3\x80\x80") == 0)
|
||||
i += 3;
|
||||
else
|
||||
result += input[i++];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static bool ValidId(const std::string &id) {
|
||||
return id.size() == 9 &&
|
||||
std::all_of(id.begin(), id.end(),
|
||||
[](unsigned char c) { return c >= '0' && c <= '9'; });
|
||||
}
|
||||
bool Add(const std::string &id, const std::string &group = {}) {
|
||||
if (!ValidId(id))
|
||||
return false;
|
||||
const bool joined = wall_ids_.insert(id).second;
|
||||
// A device grouped earlier in history joins at the end when explicitly
|
||||
// added, while retaining its existing group assignments.
|
||||
if (joined)
|
||||
std::stable_partition(members.begin(), members.end(),
|
||||
[&](const auto &m) { return m.id != id; });
|
||||
if (group.empty() && HasAssignments(id))
|
||||
return joined;
|
||||
return AddAssignment(id, group) || joined;
|
||||
}
|
||||
std::vector<std::string> Ids(const std::string &group = {}) const {
|
||||
std::vector<std::string> ids;
|
||||
std::set<std::string> seen;
|
||||
for (const auto &m : members)
|
||||
if (wall_ids_.count(m.id) && (group.empty() || m.group == group) &&
|
||||
seen.insert(m.id).second)
|
||||
ids.push_back(m.id);
|
||||
return ids;
|
||||
}
|
||||
// History filtering uses group assignments, independently of wall enrollment.
|
||||
// nullopt means all devices; an empty name means no named group.
|
||||
bool MatchesGroup(const std::string &id,
|
||||
const std::optional<std::string> &group) const {
|
||||
if (!group)
|
||||
return true;
|
||||
if (group->empty())
|
||||
return std::none_of(members.begin(), members.end(), [&](const auto &m) {
|
||||
return m.id == id && !m.group.empty();
|
||||
});
|
||||
return std::any_of(members.begin(), members.end(), [&](const auto &m) {
|
||||
return m.id == id && m.group == *group;
|
||||
});
|
||||
}
|
||||
size_t AddMany(const std::vector<std::string> &ids,
|
||||
const std::string &group = {}) {
|
||||
size_t added = 0;
|
||||
for (const auto &id : ids) {
|
||||
// Without a target group, an existing membership already puts the device
|
||||
// on the wall. An explicit group may reuse that same device/session.
|
||||
if (group.empty() && wall_ids_.count(id))
|
||||
continue;
|
||||
if (Add(id, group))
|
||||
++added;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
// Both editors replace the same group assignments without changing wall
|
||||
// enrollment. Keep the position of devices already on the wall untouched.
|
||||
bool SetGroups(const std::string &id, const std::vector<std::string> &names) {
|
||||
if (!ValidId(id))
|
||||
return false;
|
||||
std::vector<Member> replacements;
|
||||
std::set<std::string> seen;
|
||||
for (const auto &name : names)
|
||||
if (!name.empty() && seen.insert(name).second)
|
||||
replacements.push_back({id, name});
|
||||
if (replacements.empty())
|
||||
replacements.push_back({id, ""});
|
||||
std::vector<Member> updated;
|
||||
bool inserted = false;
|
||||
for (const auto &member : members) {
|
||||
if (member.id != id) {
|
||||
updated.push_back(member);
|
||||
} else if (!inserted) {
|
||||
updated.insert(updated.end(), replacements.begin(), replacements.end());
|
||||
inserted = true;
|
||||
}
|
||||
}
|
||||
if (!inserted)
|
||||
updated.insert(updated.end(), replacements.begin(), replacements.end());
|
||||
members = std::move(updated);
|
||||
for (const auto &member : replacements)
|
||||
if (!member.group.empty() &&
|
||||
std::find(groups.begin(), groups.end(), member.group) == groups.end())
|
||||
groups.push_back(member.group);
|
||||
PruneGroups();
|
||||
return true;
|
||||
}
|
||||
void Remove(const std::string &id, const std::string &group = {}) {
|
||||
if (group.empty()) {
|
||||
// Removing from the wall leaves history's group assignments intact.
|
||||
wall_ids_.erase(id);
|
||||
} else if (MatchesGroup(id, group)) {
|
||||
// Removing the last named group leaves an ungrouped wall member.
|
||||
// Reuse the editor path so enrollment and card order stay unchanged.
|
||||
std::vector<std::string> remaining;
|
||||
for (const auto &member : members)
|
||||
if (member.id == id && member.group != group)
|
||||
remaining.push_back(member.group);
|
||||
SetGroups(id, remaining);
|
||||
}
|
||||
selected.erase(id);
|
||||
}
|
||||
void ResizePage(int new_capacity, int count) {
|
||||
const int first = page * capacity;
|
||||
capacity = std::max(1, new_capacity);
|
||||
page = std::clamp(first / capacity, 0, std::max(0, (count - 1) / capacity));
|
||||
}
|
||||
// The caller supplies every ID in the current filter, across all pages.
|
||||
void ToggleSelectAll(const std::vector<std::string> &ids) {
|
||||
std::set<std::string> targets(ids.begin(), ids.end());
|
||||
if (selected == targets)
|
||||
selected.clear();
|
||||
else
|
||||
selected = std::move(targets);
|
||||
}
|
||||
bool Load(const std::filesystem::path &path) {
|
||||
try {
|
||||
if (!std::filesystem::exists(path))
|
||||
return true;
|
||||
std::ifstream file(path);
|
||||
const auto doc = nlohmann::json::parse(file);
|
||||
const int version = doc.at("version").get<int>();
|
||||
if ((version != 1 && version != 2) || !doc.at("members").is_array())
|
||||
return false;
|
||||
if (version == 2 && !doc.at("wall_ids").is_array())
|
||||
return false;
|
||||
Workspace loaded;
|
||||
for (const auto &member : doc.at("members")) {
|
||||
const auto id = member.at("id").get<std::string>();
|
||||
loaded.AddAssignment(id, member.value("group", ""));
|
||||
// Every saved member in the old format was already on the wall.
|
||||
if (version == 1 && ValidId(id))
|
||||
loaded.wall_ids_.insert(id);
|
||||
}
|
||||
if (version == 2)
|
||||
for (const auto &value : doc.at("wall_ids")) {
|
||||
const auto id = value.get<std::string>();
|
||||
if (ValidId(id) && loaded.HasAssignments(id))
|
||||
loaded.wall_ids_.insert(id);
|
||||
}
|
||||
members = std::move(loaded.members);
|
||||
groups = std::move(loaded.groups);
|
||||
wall_ids_ = std::move(loaded.wall_ids_);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool Save(const std::filesystem::path &path) const {
|
||||
try {
|
||||
nlohmann::json doc{{"version", 2},
|
||||
{"members", nlohmann::json::array()},
|
||||
{"wall_ids", wall_ids_}};
|
||||
for (const auto &m : members)
|
||||
doc["members"].push_back({{"id", m.id}, {"group", m.group}});
|
||||
return WriteAtomically(path, doc.dump(2));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::set<std::string> wall_ids_;
|
||||
|
||||
bool HasAssignments(const std::string &id) const {
|
||||
return std::any_of(members.begin(), members.end(),
|
||||
[&](const auto &m) { return m.id == id; });
|
||||
}
|
||||
bool AddAssignment(const std::string &id, const std::string &group) {
|
||||
if (!ValidId(id))
|
||||
return false;
|
||||
if (std::any_of(members.begin(), members.end(), [&](const auto &m) {
|
||||
return m.id == id && m.group == group;
|
||||
}))
|
||||
return false;
|
||||
if (!group.empty() &&
|
||||
std::find(groups.begin(), groups.end(), group) == groups.end())
|
||||
groups.push_back(group);
|
||||
members.push_back({id, group});
|
||||
return true;
|
||||
}
|
||||
void PruneGroups() {
|
||||
groups.erase(std::remove_if(groups.begin(), groups.end(),
|
||||
[&](const auto &name) {
|
||||
return std::none_of(members.begin(),
|
||||
members.end(),
|
||||
[&](const auto &m) {
|
||||
return m.group == name;
|
||||
});
|
||||
}),
|
||||
groups.end());
|
||||
}
|
||||
};
|
||||
} // namespace crossdesk::gallery
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user