v0.1.2: Preferences page, About page, full icon set, redesigned edit form

- Rework the add/edit form onto a generic, catalog-driven field model
  (JobFieldCatalog + JobEditModel), replacing the old hand-written
  ApplicationEditDialog with ApplicationEditPage
- Add a Preferences page (reachable from the hamburger menu) with a
  KColorSchemeManager-backed color scheme switcher
- Add an About page reading from KAboutData
- Ship a full hicolor icon set (16-128px + scalable) via ecm_install_icons
  instead of a single flat SVG
- Restyle the edit form after KDE System Settings' Audio page:
  Kirigami.ListSectionHeader per category, flat separated rows, no card
  borders
- Explicitly declare Kirigami/KirigamiAddons/ColorScheme as CMake
  dependencies instead of relying on the QML import scanner alone
- Add KDEClangFormat/KDEGitCommitHooks, a CMakePresets.json, and a
  REUSE + clang-format lint CI workflow
- Reformat SPDX headers to include copyright, remove debug screenshot
  scaffolding
This commit is contained in:
2026-07-03 16:25:42 -05:00
parent 753822c242
commit 078343ff96
52 changed files with 1258 additions and 294 deletions
+204
View File
@@ -0,0 +1,204 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "jobeditmodel.h"
#include <QDate>
using namespace Qt::Literals::StringLiterals;
using JobFieldCatalog::ComboRow;
using JobFieldCatalog::DateRow;
using JobFieldCatalog::Field;
using JobFieldCatalog::SpinBoxRow;
using JobFieldCatalog::TextAreaRow;
using JobFieldCatalog::TextRow;
namespace
{
/// The three salary fields share the "0 shown in the UI means unset, -1 stored" convention.
bool isSalaryField(const QString &id)
{
return id == QLatin1String("salaryMin") || id == QLatin1String("salaryMax") || id == QLatin1String("salaryExpectation");
}
}
JobEditModel::JobEditModel(QObject *parent)
: QAbstractListModel(parent)
, m_fields(JobFieldCatalog::fields())
{
resetValues();
}
int JobEditModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_fields.size();
}
QVariant JobEditModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_fields.size()) {
return {};
}
const Field &field = m_fields.at(index.row());
switch (role) {
case FieldIdRole:
return field.id;
case CategoryIdRole:
return field.categoryId;
case LabelRole:
return field.label;
case RowTypeRole:
return static_cast<int>(field.rowType);
case ValueRole:
return m_values.value(field.id);
case ComboOptionsRole:
return field.comboOptions;
case PlaceholderRole:
return field.placeholder;
case SpinMinRole:
return field.spinMin;
case SpinMaxRole:
return field.spinMax;
default:
return {};
}
}
QHash<int, QByteArray> JobEditModel::roleNames() const
{
return {
{FieldIdRole, "fieldId"},
{CategoryIdRole, "categoryId"},
{LabelRole, "label"},
{RowTypeRole, "rowType"},
{ValueRole, "value"},
{ComboOptionsRole, "comboOptions"},
{PlaceholderRole, "placeholder"},
{SpinMinRole, "spinMin"},
{SpinMaxRole, "spinMax"},
};
}
JobsModel *JobEditModel::jobsModel() const
{
return m_jobsModel;
}
void JobEditModel::setJobsModel(JobsModel *model)
{
if (m_jobsModel == model) {
return;
}
m_jobsModel = model;
Q_EMIT jobsModelChanged();
}
int JobEditModel::editingJobId() const
{
return m_editingJobId;
}
void JobEditModel::setEditingJobId(int id)
{
if (m_editingJobId == id) {
return;
}
m_editingJobId = id;
resetValues();
Q_EMIT editingJobIdChanged();
}
void JobEditModel::resetValues()
{
m_values.clear();
if (m_editingJobId < 0 || !m_jobsModel) {
m_values[u"currency"_s] = u"USD"_s;
m_values[u"stage"_s] = u"Applied"_s;
m_values[u"dateApplied"_s] = QDate::currentDate();
m_values[u"remoteType"_s] = u"Unspecified"_s;
m_values[u"salaryMin"_s] = 0;
m_values[u"salaryMax"_s] = 0;
m_values[u"salaryExpectation"_s] = 0;
} else {
const QVariantMap data = m_jobsModel->jobData(m_editingJobId);
for (const Field &field : m_fields) {
QVariant value = data.value(field.id);
if (field.id == u"remoteType"_s && value.toString().isEmpty()) {
value = u"Unspecified"_s;
}
if (isSalaryField(field.id) && value.toInt() < 0) {
value = 0;
}
m_values[field.id] = value;
}
}
if (rowCount() > 0) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1));
}
}
QVariantList JobEditModel::categories() const
{
QVariantList result;
for (const JobFieldCatalog::Category &category : JobFieldCatalog::categories()) {
result.append(QVariantMap{{u"id"_s, category.id}, {u"title"_s, category.title}});
}
return result;
}
QString JobEditModel::lastError() const
{
return m_lastError;
}
void JobEditModel::setValue(int row, const QVariant &value)
{
if (row < 0 || row >= m_fields.size()) {
return;
}
m_values[m_fields.at(row).id] = value;
Q_EMIT dataChanged(index(row), index(row), {ValueRole});
}
bool JobEditModel::save()
{
if (!m_jobsModel) {
return false;
}
QVariantMap fields;
for (auto it = m_values.constBegin(); it != m_values.constEnd(); ++it) {
fields.insert(it.key(), it.value());
}
if (fields.value(u"remoteType"_s).toString() == u"Unspecified"_s) {
fields[u"remoteType"_s] = QString();
}
for (const QString &id : {u"salaryMin"_s, u"salaryMax"_s, u"salaryExpectation"_s}) {
if (fields.value(id).toInt() <= 0) {
fields[id] = -1;
}
}
const bool ok = m_editingJobId < 0 ? m_jobsModel->addJob(fields) : m_jobsModel->updateJob(m_editingJobId, fields);
if (!ok) {
m_lastError = m_jobsModel->lastError();
Q_EMIT lastErrorChanged();
}
return ok;
}
bool JobEditModel::deleteJob()
{
if (!m_jobsModel || m_editingJobId < 0) {
return false;
}
return m_jobsModel->removeJob(m_editingJobId);
}