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
@@ -0,0 +1,16 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
SPDX-License-Identifier: CC0-1.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Reason for the change
|
||||||
|
|
||||||
|
<!-- What problem does this solve, or what does it add? -->
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
|
||||||
|
<!-- How did you verify this? Build/run steps, commands, etc. -->
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!-- If this changes UI, include a before/after. -->
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
name: Build and Publish Flatpak
|
name: Build and Publish Flatpak
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
reuse:
|
||||||
|
name: REUSE compliance
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: REUSE Compliance Check
|
||||||
|
uses: fsfe/reuse-action@v3
|
||||||
|
|
||||||
|
clang-format:
|
||||||
|
name: clang-format
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.10
|
||||||
|
options: --privileged
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure (generates the KDE .clang-format style)
|
||||||
|
run: cmake -B build -G Ninja -DBUILD_TESTING=OFF
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: |
|
||||||
|
find src autotests -name '*.h' -o -name '*.cpp' | \
|
||||||
|
xargs clang-format --dry-run --Werror
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
name: Build and Test
|
name: Build and Test
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,5 @@
|
|||||||
# Editor / misc
|
# Editor / misc
|
||||||
*.user
|
*.user
|
||||||
*.autosave
|
*.autosave
|
||||||
|
# Generated by ECM's KDEClangFormat module
|
||||||
|
/.clang-format
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# Kareer - a Kirigami/Qt job application tracker
|
# Kareer - a Kirigami/Qt job application tracker
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(kareer VERSION 0.1.1 LANGUAGES CXX)
|
project(kareer VERSION 0.1.2 LANGUAGES CXX)
|
||||||
|
|
||||||
set(REQUIRED_QT_VERSION 6.6.0)
|
set(REQUIRED_QT_VERSION 6.6.0)
|
||||||
set(REQUIRED_KF_VERSION 6.5.0)
|
set(REQUIRED_KF_VERSION 6.5.0)
|
||||||
@@ -15,7 +17,10 @@ include(KDEInstallDirs)
|
|||||||
include(KDECMakeSettings)
|
include(KDECMakeSettings)
|
||||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
include(ECMSetupVersion)
|
include(ECMSetupVersion)
|
||||||
|
include(ECMInstallIcons)
|
||||||
include(FeatureSummary)
|
include(FeatureSummary)
|
||||||
|
include(KDEClangFormat)
|
||||||
|
include(KDEGitCommitHooks)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -38,6 +43,19 @@ find_package(KF6 ${REQUIRED_KF_VERSION} REQUIRED COMPONENTS
|
|||||||
CoreAddons
|
CoreAddons
|
||||||
IconThemes
|
IconThemes
|
||||||
Crash
|
Crash
|
||||||
|
ColorScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
find_package(KF6Kirigami ${REQUIRED_KF_VERSION} REQUIRED)
|
||||||
|
set_package_properties(KF6Kirigami PROPERTIES
|
||||||
|
TYPE REQUIRED
|
||||||
|
DESCRIPTION "KDE's next-gen UI framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
find_package(KF6KirigamiAddons REQUIRED)
|
||||||
|
set_package_properties(KF6KirigamiAddons PROPERTIES
|
||||||
|
TYPE REQUIRED
|
||||||
|
DESCRIPTION "Add-on components for Kirigami"
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_setup_version(${PROJECT_VERSION}
|
ecm_setup_version(${PROJECT_VERSION}
|
||||||
@@ -45,6 +63,7 @@ ecm_setup_version(${PROJECT_VERSION}
|
|||||||
VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kareer-version.h"
|
VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kareer-version.h"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_subdirectory(icons)
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
@@ -55,11 +74,11 @@ install(PROGRAMS io.github.toservetheking.Kareer.desktop
|
|||||||
DESTINATION ${KDE_INSTALL_APPDIR})
|
DESTINATION ${KDE_INSTALL_APPDIR})
|
||||||
install(FILES io.github.toservetheking.Kareer.metainfo.xml
|
install(FILES io.github.toservetheking.Kareer.metainfo.xml
|
||||||
DESTINATION ${KDE_INSTALL_METAINFODIR})
|
DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||||
install(FILES icons/io.github.toservetheking.Kareer.svg
|
|
||||||
DESTINATION ${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps)
|
|
||||||
install(FILES LICENSES/GPL-3.0-or-later.txt
|
install(FILES LICENSES/GPL-3.0-or-later.txt
|
||||||
DESTINATION ${KDE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME})
|
DESTINATION ${KDE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME})
|
||||||
|
|
||||||
# Translations: add a po/ directory and re-enable ki18n_install(po) once present.
|
# Translations: add a po/ directory and re-enable ki18n_install(po) once present.
|
||||||
|
|
||||||
|
kde_configure_git_pre_commit_hook(CHECKS CLANG-FORMAT)
|
||||||
|
|
||||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": 6,
|
||||||
|
"cmakeMinimumRequired": {
|
||||||
|
"major": 3,
|
||||||
|
"minor": 23,
|
||||||
|
"patch": 0
|
||||||
|
},
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "ninja-dev",
|
||||||
|
"displayName": "Ninja (dev)",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
|
"BUILD_TESTING": "ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
SPDX-License-Identifier: CC0-1.0
|
||||||
@@ -102,7 +102,8 @@ CMake toolchain. On Arch/CachyOS:
|
|||||||
```sh
|
```sh
|
||||||
sudo pacman -S --needed cmake extra-cmake-modules base-devel \
|
sudo pacman -S --needed cmake extra-cmake-modules base-devel \
|
||||||
qt6-base qt6-declarative kirigami kirigami-addons \
|
qt6-base qt6-declarative kirigami kirigami-addons \
|
||||||
ki18n kcoreaddons kiconthemes kcrash kitemmodels qqc2-desktop-style
|
ki18n kcoreaddons kiconthemes kcrash kitemmodels \
|
||||||
|
kcolorscheme qqc2-desktop-style
|
||||||
```
|
```
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
@@ -128,8 +129,11 @@ Run the tests with `ctest --test-dir build`.
|
|||||||
- `sankeymodel.{h,cpp}` - turns stage history into laid-out Sankey
|
- `sankeymodel.{h,cpp}` - turns stage history into laid-out Sankey
|
||||||
geometry (node columns/stacking, ribbon SVG path data); QML only
|
geometry (node columns/stacking, ribbon SVG path data); QML only
|
||||||
draws what this hands back.
|
draws what this hands back.
|
||||||
|
- `jobfieldcatalog.{h,cpp}` - the static catalog of edit-form fields and categories.
|
||||||
|
- `jobeditmodel.{h,cpp}` - `QAbstractListModel`-backed edit-form state, built from the field catalog.
|
||||||
- `clicommands.{h,cpp}` - the `add`/`list`/`show`/`update`/`stage`/
|
- `clicommands.{h,cpp}` - the `add`/`list`/`show`/`update`/`stage`/
|
||||||
`delete`/`stats`/`stages` subcommands.
|
`delete`/`stats`/`stages` subcommands.
|
||||||
|
- `appcolorscheme.{h,cpp}` - QML-facing wrapper around `KColorSchemeManager` for the Preferences page.
|
||||||
- `qml/` - Kirigami UI: `ApplicationsPage` (list + search),
|
- `qml/` - Kirigami UI: `ApplicationsPage` (list + search),
|
||||||
`ApplicationEditDialog` (add/edit/delete form), `DashboardPage`
|
`ApplicationEditPage` (add/edit/delete form), `DashboardPage`
|
||||||
(stat cards + pipeline), `SankeyDiagram` (the renderer).
|
(stat cards + pipeline), `SankeyDiagram` (the renderer).
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ SPDX-PackageDownloadLocation = "https://github.com/toservetheking/Kareer"
|
|||||||
path = [
|
path = [
|
||||||
"io.github.toservetheking.Kareer.desktop",
|
"io.github.toservetheking.Kareer.desktop",
|
||||||
"keys/*.asc",
|
"keys/*.asc",
|
||||||
|
"icons/*.png",
|
||||||
]
|
]
|
||||||
SPDX-License-Identifier = "GPL-3.0-or-later"
|
SPDX-License-Identifier = "GPL-3.0-or-later"
|
||||||
SPDX-FileCopyrightText = "ToServeTheKing <[email protected]>"
|
SPDX-FileCopyrightText = "ToServeTheKing <[email protected]>"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
add_executable(jobsdatabasetest
|
add_executable(jobsdatabasetest
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "job.h"
|
#include "job.h"
|
||||||
#include "jobsdatabase.h"
|
#include "jobsdatabase.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "job.h"
|
#include "job.h"
|
||||||
#include "jobsdatabase.h"
|
#include "jobsdatabase.h"
|
||||||
#include "sankeymodel.h"
|
#include "sankeymodel.h"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 572 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
ecm_install_icons(ICONS
|
||||||
|
sc-apps-kareer.svg
|
||||||
|
128-apps-kareer.png
|
||||||
|
64-apps-kareer.png
|
||||||
|
48-apps-kareer.png
|
||||||
|
44-apps-kareer.png
|
||||||
|
32-apps-kareer.png
|
||||||
|
22-apps-kareer.png
|
||||||
|
16-apps-kareer.png
|
||||||
|
DESTINATION ${KDE_INSTALL_ICONDIR}
|
||||||
|
THEME hicolor
|
||||||
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]> -->
|
||||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -64,6 +64,17 @@
|
|||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.1.2" date="2026-07-03">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Rework the add/edit form onto a generic, catalog-driven field model</li>
|
||||||
|
<li>Add a Preferences page with a color-scheme switcher</li>
|
||||||
|
<li>Add an About page</li>
|
||||||
|
<li>Ship a full hicolor icon set instead of a single scalable SVG</li>
|
||||||
|
<li>Match KDE System Settings' Audio page layout for the edit form</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.1.1" date="2026-07-03">
|
<release version="0.1.1" date="2026-07-03">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
id: io.github.toservetheking.Kareer
|
id: io.github.toservetheking.Kareer
|
||||||
runtime: org.kde.Platform
|
runtime: org.kde.Platform
|
||||||
@@ -27,12 +29,12 @@ modules:
|
|||||||
- -DCMAKE_BUILD_TYPE=Release
|
- -DCMAKE_BUILD_TYPE=Release
|
||||||
run-tests: true
|
run-tests: true
|
||||||
sources:
|
sources:
|
||||||
# For Flathub / release builds, pin to a tag AND commit:
|
# For local testing before the repo is pushed:
|
||||||
- type: git
|
- type: dir
|
||||||
url: https://github.com/toservetheking/Kareer.git
|
path: .
|
||||||
tag: v0.1.1
|
|
||||||
# commit: <fill in the exact commit SHA the tag points at>
|
|
||||||
#
|
#
|
||||||
# For local testing before the repo is pushed, replace the source above with:
|
# For Flathub / release builds, pin to a tag AND commit instead:
|
||||||
# - type: dir
|
# - type: git
|
||||||
# path: .
|
# url: https://github.com/toservetheking/Kareer.git
|
||||||
|
# tag: v0.1.1
|
||||||
|
# commit: <fill in the exact commit SHA the tag points at>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
add_executable(kareer
|
add_executable(kareer
|
||||||
main.cpp
|
main.cpp
|
||||||
jobstage.cpp
|
jobstage.cpp
|
||||||
jobsdatabase.cpp
|
jobsdatabase.cpp
|
||||||
|
jobfieldcatalog.cpp
|
||||||
clicommands.cpp
|
clicommands.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,9 +17,10 @@ qt_add_qml_module(kareer
|
|||||||
QML_FILES
|
QML_FILES
|
||||||
qml/Main.qml
|
qml/Main.qml
|
||||||
qml/ApplicationsPage.qml
|
qml/ApplicationsPage.qml
|
||||||
qml/ApplicationEditDialog.qml
|
qml/ApplicationEditPage.qml
|
||||||
qml/DashboardPage.qml
|
qml/DashboardPage.qml
|
||||||
qml/SankeyDiagram.qml
|
qml/SankeyDiagram.qml
|
||||||
|
qml/SettingsPage.qml
|
||||||
SOURCES
|
SOURCES
|
||||||
jobsmodel.cpp
|
jobsmodel.cpp
|
||||||
jobsmodel.h
|
jobsmodel.h
|
||||||
@@ -24,6 +28,10 @@ qt_add_qml_module(kareer
|
|||||||
statsmodel.h
|
statsmodel.h
|
||||||
sankeymodel.cpp
|
sankeymodel.cpp
|
||||||
sankeymodel.h
|
sankeymodel.h
|
||||||
|
jobeditmodel.cpp
|
||||||
|
jobeditmodel.h
|
||||||
|
appcolorscheme.cpp
|
||||||
|
appcolorscheme.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(kareer PRIVATE ${CMAKE_BINARY_DIR})
|
target_include_directories(kareer PRIVATE ${CMAKE_BINARY_DIR})
|
||||||
@@ -41,6 +49,8 @@ target_link_libraries(kareer PRIVATE
|
|||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
KF6::IconThemes
|
KF6::IconThemes
|
||||||
KF6::Crash
|
KF6::Crash
|
||||||
|
KF6::ColorScheme
|
||||||
|
KF6::Kirigami
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS kareer ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
install(TARGETS kareer ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "appcolorscheme.h"
|
||||||
|
|
||||||
|
#include <KColorSchemeManager>
|
||||||
|
|
||||||
|
AppColorScheme::AppColorScheme(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, mSchemes(KColorSchemeManager::instance())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QAbstractItemModel *AppColorScheme::colorSchemesModel()
|
||||||
|
{
|
||||||
|
return mSchemes->model();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AppColorScheme::activeColorSchemeName() const
|
||||||
|
{
|
||||||
|
return mSchemes->activeSchemeName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppColorScheme::setActiveColorSchemeName(const QString &name)
|
||||||
|
{
|
||||||
|
if (name == activeColorSchemeName()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mSchemes->activateScheme(mSchemes->indexForScheme(name));
|
||||||
|
Q_EMIT activeColorSchemeNameChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
class KColorSchemeManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin QML-facing wrapper around KColorSchemeManager: exposes the list of
|
||||||
|
* installed color schemes and the currently active one. KColorSchemeManager
|
||||||
|
* autosaves the active scheme itself, so there is nothing else to persist.
|
||||||
|
*/
|
||||||
|
class AppColorScheme : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
Q_PROPERTY(QAbstractItemModel *colorSchemesModel READ colorSchemesModel CONSTANT)
|
||||||
|
Q_PROPERTY(QString activeColorSchemeName READ activeColorSchemeName WRITE setActiveColorSchemeName NOTIFY activeColorSchemeNameChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AppColorScheme(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QAbstractItemModel *colorSchemesModel();
|
||||||
|
QString activeColorSchemeName() const;
|
||||||
|
void setActiveColorSchemeName(const QString &name);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void activeColorSchemeNameChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
KColorSchemeManager *mSchemes;
|
||||||
|
};
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "clicommands.h"
|
#include "clicommands.h"
|
||||||
|
|
||||||
#include "job.h"
|
#include "job.h"
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QDate>
|
#include <QDate>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "jobfieldcatalog.h"
|
||||||
|
#include "jobsmodel.h"
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drives the add/edit form the way FlatKontrol's PermissionsController drives
|
||||||
|
* PermissionsPage: a generic row model (one row per JobFieldCatalog::Field)
|
||||||
|
* that QML renders via Repeater + DelegateChooser on rowType, instead of
|
||||||
|
* each field being hand-written in QML.
|
||||||
|
*/
|
||||||
|
class JobEditModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(JobsModel *jobsModel READ jobsModel WRITE setJobsModel NOTIFY jobsModelChanged)
|
||||||
|
Q_PROPERTY(int editingJobId READ editingJobId WRITE setEditingJobId NOTIFY editingJobIdChanged)
|
||||||
|
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
|
||||||
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
FieldIdRole = Qt::UserRole + 1,
|
||||||
|
CategoryIdRole,
|
||||||
|
LabelRole,
|
||||||
|
RowTypeRole,
|
||||||
|
ValueRole,
|
||||||
|
ComboOptionsRole,
|
||||||
|
PlaceholderRole,
|
||||||
|
SpinMinRole,
|
||||||
|
SpinMaxRole,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
|
explicit JobEditModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
JobsModel *jobsModel() const;
|
||||||
|
void setJobsModel(JobsModel *model);
|
||||||
|
|
||||||
|
int editingJobId() const;
|
||||||
|
void setEditingJobId(int id);
|
||||||
|
|
||||||
|
QVariantList categories() const;
|
||||||
|
QString lastError() const;
|
||||||
|
|
||||||
|
/// Updates the value for the field at this row (called from the QML delegate).
|
||||||
|
Q_INVOKABLE void setValue(int row, const QVariant &value);
|
||||||
|
|
||||||
|
/// Persists the current values via jobsModel; true on success.
|
||||||
|
Q_INVOKABLE bool save();
|
||||||
|
|
||||||
|
Q_INVOKABLE bool deleteJob();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void jobsModelChanged();
|
||||||
|
void editingJobIdChanged();
|
||||||
|
void lastErrorChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resetValues();
|
||||||
|
|
||||||
|
JobsModel *m_jobsModel = nullptr;
|
||||||
|
int m_editingJobId = -1;
|
||||||
|
QHash<QString, QVariant> m_values;
|
||||||
|
QString m_lastError;
|
||||||
|
QList<JobFieldCatalog::Field> m_fields;
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "jobfieldcatalog.h"
|
||||||
|
#include "jobstage.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
namespace JobFieldCatalog
|
||||||
|
{
|
||||||
|
|
||||||
|
QList<Category> categories()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{u"company"_s, QStringLiteral("Company")},
|
||||||
|
{u"pipeline"_s, QStringLiteral("Pipeline")},
|
||||||
|
{u"salary"_s, QStringLiteral("Salary")},
|
||||||
|
{u"details"_s, QStringLiteral("Additional Details")},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Field> fields()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{u"company"_s, u"company"_s, QStringLiteral("Company:"), TextRow, {}, {}, 0, 0},
|
||||||
|
{u"title"_s, u"company"_s, QStringLiteral("Job Title:"), TextRow, {}, {}, 0, 0},
|
||||||
|
{u"location"_s, u"company"_s, QStringLiteral("Location:"), TextRow, {}, {}, 0, 0},
|
||||||
|
{u"remoteType"_s, u"company"_s, QStringLiteral("Remote Type:"), ComboRow, {QStringLiteral("Unspecified"), QStringLiteral("Onsite"), QStringLiteral("Hybrid"), QStringLiteral("Remote")}, {}, 0, 0},
|
||||||
|
|
||||||
|
{u"stage"_s, u"pipeline"_s, QStringLiteral("Stage:"), ComboRow, JobStage::canonicalStages(), {}, 0, 0},
|
||||||
|
{u"dateApplied"_s, u"pipeline"_s, QStringLiteral("Date Applied:"), DateRow, {}, {}, 0, 0},
|
||||||
|
|
||||||
|
{u"salaryMin"_s, u"salary"_s, QStringLiteral("Range Minimum:"), SpinBoxRow, {}, {}, 0, 5000000},
|
||||||
|
{u"salaryMax"_s, u"salary"_s, QStringLiteral("Range Maximum:"), SpinBoxRow, {}, {}, 0, 5000000},
|
||||||
|
{u"salaryExpectation"_s, u"salary"_s, QStringLiteral("Your Expectation:"), SpinBoxRow, {}, {}, 0, 5000000},
|
||||||
|
{u"currency"_s, u"salary"_s, QStringLiteral("Currency:"), TextRow, {}, {}, 0, 0},
|
||||||
|
|
||||||
|
{u"source"_s, u"details"_s, QStringLiteral("Source:"), TextRow, {}, QStringLiteral("Referral, LinkedIn, company site..."), 0, 0},
|
||||||
|
{u"url"_s, u"details"_s, QStringLiteral("Job Posting URL:"), TextRow, {}, {}, 0, 0},
|
||||||
|
{u"contact"_s, u"details"_s, QStringLiteral("Contact:"), TextRow, {}, {}, 0, 0},
|
||||||
|
{u"notes"_s, u"details"_s, QStringLiteral("Notes:"), TextAreaRow, {}, {}, 0, 0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The static structure of the job edit form: which categories exist, and
|
||||||
|
* which fields belong to each, in display order. Mirrors FlatKontrol's
|
||||||
|
* PermissionCatalog - JobEditModel supplies the per-job values, this
|
||||||
|
* supplies the shape.
|
||||||
|
*/
|
||||||
|
namespace JobFieldCatalog
|
||||||
|
{
|
||||||
|
enum RowType {
|
||||||
|
TextRow = 0,
|
||||||
|
ComboRow,
|
||||||
|
SpinBoxRow,
|
||||||
|
DateRow,
|
||||||
|
TextAreaRow,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Category {
|
||||||
|
QString id;
|
||||||
|
QString title;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Field {
|
||||||
|
QString id; ///< Matches a Job/JobsModel field key (see JobsModel::mapFromJob).
|
||||||
|
QString categoryId;
|
||||||
|
QString label;
|
||||||
|
RowType rowType;
|
||||||
|
QStringList comboOptions; ///< Only meaningful for ComboRow.
|
||||||
|
QString placeholder; ///< Only meaningful for TextRow.
|
||||||
|
int spinMin = 0; ///< Only meaningful for SpinBoxRow.
|
||||||
|
int spinMax = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
QList<Category> categories();
|
||||||
|
QList<Field> fields();
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "jobsdatabase.h"
|
#include "jobsdatabase.h"
|
||||||
#include "jobstage.h"
|
#include "jobstage.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "job.h"
|
#include "job.h"
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "jobsmodel.h"
|
#include "jobsmodel.h"
|
||||||
#include "jobstage.h"
|
#include "jobstage.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "job.h"
|
#include "job.h"
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "jobstage.h"
|
#include "jobstage.h"
|
||||||
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "kareer-version.h"
|
#include "kareer-version.h"
|
||||||
|
|
||||||
#include "clicommands.h"
|
#include "clicommands.h"
|
||||||
@@ -20,24 +25,37 @@ using namespace Qt::Literals::StringLiterals;
|
|||||||
|
|
||||||
// Filter out a couple of well-known benign framework artifacts rather than
|
// Filter out a couple of well-known benign framework artifacts rather than
|
||||||
// spamming every run. Everything else is passed through untouched.
|
// spamming every run. Everything else is passed through untouched.
|
||||||
static QtMessageHandler s_defaultMessageHandler = nullptr;
|
static bool isBenignFrameworkNoise(const QString &message)
|
||||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
|
||||||
{
|
{
|
||||||
// Qt Quick emits this while Kirigami's PageRow incubates pages. It fires even
|
// Qt Quick emits this while Kirigami's PageRow incubates pages. It fires even
|
||||||
// for a trivial empty page, is harmless, and cannot be avoided from app code.
|
// for a trivial empty page, is harmless, and cannot be avoided from app code.
|
||||||
if (message.contains(QLatin1String("was not placed in the graphics scene"))) {
|
if (message.contains(QLatin1String("was not placed in the graphics scene"))) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
// Malformed path data in third-party icon SVGs (system/app icon themes) is not
|
// An internal PageRow/StackView implementation detail, not something app
|
||||||
// actionable from here; drop the noise rather than spam every render.
|
// code can influence.
|
||||||
if (context.category && qstrcmp(context.category, "qt.svg") == 0) {
|
if (message.contains(QLatin1String("StackView has detected conflicting anchors"))) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
// Qt's Wayland integration tries to self-register with xdg-desktop-portal for
|
// Qt's Wayland integration tries to self-register with xdg-desktop-portal for
|
||||||
// optional desktop features (global shortcuts, background). Kareer doesn't use
|
// optional desktop features (global shortcuts, background). Kareer doesn't use
|
||||||
// any of those, and it fires harmlessly on hosts where portal app-info
|
// any of those, and it fires harmlessly on hosts where portal app-info
|
||||||
// resolution is finicky.
|
// resolution is finicky.
|
||||||
if (message.contains(QLatin1String("Failed to register with host portal"))) {
|
if (message.contains(QLatin1String("Failed to register with host portal"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QtMessageHandler s_defaultMessageHandler = nullptr;
|
||||||
|
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||||
|
{
|
||||||
|
if (isBenignFrameworkNoise(message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Malformed path data in third-party icon SVGs (system/app icon themes) is not
|
||||||
|
// actionable from here; drop the noise rather than spam every render.
|
||||||
|
if (context.category && qstrcmp(context.category, "qt.svg") == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (s_defaultMessageHandler) {
|
if (s_defaultMessageHandler) {
|
||||||
@@ -87,6 +105,20 @@ int main(int argc, char *argv[])
|
|||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
KLocalization::setupLocalizedContext(&engine);
|
KLocalization::setupLocalizedContext(&engine);
|
||||||
|
|
||||||
|
QObject::connect(&engine, &QQmlApplicationEngine::warnings, &engine, [](const QList<QQmlError> &warnings) {
|
||||||
|
for (const QQmlError &error : warnings) {
|
||||||
|
if (isBenignFrameworkNoise(error.description())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "QML-WARNING: %s\n", qPrintable(error.toString()));
|
||||||
|
}
|
||||||
|
fflush(stderr);
|
||||||
|
});
|
||||||
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &engine, [](const QUrl &url) {
|
||||||
|
fprintf(stderr, "QML-OBJECT-CREATION-FAILED: %s\n", qPrintable(url.toString()));
|
||||||
|
fflush(stderr);
|
||||||
|
});
|
||||||
|
|
||||||
engine.loadFromModule("io.github.toservetheking.Kareer", u"Main"_s);
|
engine.loadFromModule("io.github.toservetheking.Kareer", u"Main"_s);
|
||||||
if (engine.rootObjects().isEmpty()) {
|
if (engine.rootObjects().isEmpty()) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls as QQC2
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
import org.kde.kirigamiaddons.formcard as FormCard
|
|
||||||
import io.github.toservetheking.Kareer
|
|
||||||
|
|
||||||
FormCard.FormCardDialog {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property JobsModel jobsModel
|
|
||||||
property int editingJobId: -1
|
|
||||||
|
|
||||||
title: editingJobId < 0 ? i18nc("@title:dialog", "Add Application") : i18nc("@title:dialog", "Edit Application")
|
|
||||||
standardButtons: QQC2.Dialog.Save | QQC2.Dialog.Cancel
|
|
||||||
|
|
||||||
function openForAdd(): void {
|
|
||||||
editingJobId = -1;
|
|
||||||
companyField.text = "";
|
|
||||||
titleField.text = "";
|
|
||||||
locationField.text = "";
|
|
||||||
remoteCombo.currentIndex = 0;
|
|
||||||
sourceField.text = "";
|
|
||||||
urlField.text = "";
|
|
||||||
dateField.value = new Date();
|
|
||||||
salaryMinField.value = 0;
|
|
||||||
salaryMaxField.value = 0;
|
|
||||||
salaryExpectationField.value = 0;
|
|
||||||
currencyField.text = "USD";
|
|
||||||
contactField.text = "";
|
|
||||||
notesField.text = "";
|
|
||||||
const stages = root.jobsModel.stages;
|
|
||||||
stageCombo.currentIndex = stages.indexOf("Applied");
|
|
||||||
errorLabel.text = "";
|
|
||||||
root.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openForEdit(id: int): void {
|
|
||||||
editingJobId = id;
|
|
||||||
const data = root.jobsModel.jobData(id);
|
|
||||||
companyField.text = data.company;
|
|
||||||
titleField.text = data.title;
|
|
||||||
locationField.text = data.location;
|
|
||||||
remoteCombo.currentIndex = Math.max(0, remoteCombo.model.indexOf(data.remoteType));
|
|
||||||
sourceField.text = data.source;
|
|
||||||
urlField.text = data.url;
|
|
||||||
dateField.value = data.dateApplied;
|
|
||||||
salaryMinField.value = data.salaryMin > 0 ? data.salaryMin : 0;
|
|
||||||
salaryMaxField.value = data.salaryMax > 0 ? data.salaryMax : 0;
|
|
||||||
salaryExpectationField.value = data.salaryExpectation > 0 ? data.salaryExpectation : 0;
|
|
||||||
currencyField.text = data.currency;
|
|
||||||
contactField.text = data.contact;
|
|
||||||
notesField.text = data.notes;
|
|
||||||
const stages = root.jobsModel.stages;
|
|
||||||
stageCombo.currentIndex = Math.max(0, stages.indexOf(data.stage));
|
|
||||||
errorLabel.text = "";
|
|
||||||
root.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
onAccepted: {
|
|
||||||
const fields = {
|
|
||||||
company: companyField.text,
|
|
||||||
title: titleField.text,
|
|
||||||
location: locationField.text,
|
|
||||||
remoteType: remoteCombo.currentIndex === 0 ? "" : remoteCombo.currentText,
|
|
||||||
source: sourceField.text,
|
|
||||||
url: urlField.text,
|
|
||||||
dateApplied: dateField.value,
|
|
||||||
salaryMin: salaryMinField.value > 0 ? salaryMinField.value : -1,
|
|
||||||
salaryMax: salaryMaxField.value > 0 ? salaryMaxField.value : -1,
|
|
||||||
salaryExpectation: salaryExpectationField.value > 0 ? salaryExpectationField.value : -1,
|
|
||||||
currency: currencyField.text,
|
|
||||||
contact: contactField.text,
|
|
||||||
notes: notesField.text,
|
|
||||||
stage: stageCombo.currentText,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ok = root.editingJobId < 0 ? root.jobsModel.addJob(fields) : root.jobsModel.updateJob(root.editingJobId, fields);
|
|
||||||
if (!ok) {
|
|
||||||
errorLabel.text = root.jobsModel.lastError();
|
|
||||||
root.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormCard {
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: companyField
|
|
||||||
label: i18nc("@label:textbox", "Company")
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: titleField
|
|
||||||
label: i18nc("@label:textbox", "Job Title")
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: locationField
|
|
||||||
label: i18nc("@label:textbox", "Location")
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormComboBoxDelegate {
|
|
||||||
id: remoteCombo
|
|
||||||
text: i18nc("@label:listbox", "Remote Type")
|
|
||||||
model: ["Unspecified", "Onsite", "Hybrid", "Remote"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormCard {
|
|
||||||
FormCard.FormComboBoxDelegate {
|
|
||||||
id: stageCombo
|
|
||||||
text: i18nc("@label:listbox", "Stage")
|
|
||||||
model: root.jobsModel.stages
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormHeader {
|
|
||||||
title: i18nc("@title:group", "Date Applied")
|
|
||||||
}
|
|
||||||
FormCard.FormDateTimeDelegate {
|
|
||||||
id: dateField
|
|
||||||
dateTimeDisplay: FormCard.FormDateTimeDelegate.DateTimeDisplay.Date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormCard {
|
|
||||||
FormCard.FormSpinBoxDelegate {
|
|
||||||
id: salaryMinField
|
|
||||||
label: i18nc("@label:spinbox", "Salary Range Minimum")
|
|
||||||
from: 0
|
|
||||||
to: 5000000
|
|
||||||
stepSize: 1000
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormSpinBoxDelegate {
|
|
||||||
id: salaryMaxField
|
|
||||||
label: i18nc("@label:spinbox", "Salary Range Maximum")
|
|
||||||
from: 0
|
|
||||||
to: 5000000
|
|
||||||
stepSize: 1000
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormSpinBoxDelegate {
|
|
||||||
id: salaryExpectationField
|
|
||||||
label: i18nc("@label:spinbox", "Your Salary Expectation")
|
|
||||||
from: 0
|
|
||||||
to: 5000000
|
|
||||||
stepSize: 1000
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: currencyField
|
|
||||||
label: i18nc("@label:textbox", "Currency")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormCard {
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: sourceField
|
|
||||||
label: i18nc("@label:textbox", "Source")
|
|
||||||
placeholderText: i18nc("@info:placeholder", "Referral, LinkedIn, company site...")
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: urlField
|
|
||||||
label: i18nc("@label:textbox", "Job Posting URL")
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: contactField
|
|
||||||
label: i18nc("@label:textbox", "Contact")
|
|
||||||
}
|
|
||||||
FormCard.FormDelegateSeparator {}
|
|
||||||
FormCard.FormTextAreaDelegate {
|
|
||||||
id: notesField
|
|
||||||
label: i18nc("@label:textbox", "Notes / Expectations")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormCard {
|
|
||||||
visible: root.editingJobId >= 0
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
text: i18nc("@action:button", "Delete Application")
|
|
||||||
icon.name: "edit-delete"
|
|
||||||
onClicked: deleteConfirmDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
|
||||||
id: errorLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.margins: Kirigami.Units.smallSpacing
|
|
||||||
type: Kirigami.MessageType.Error
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.PromptDialog {
|
|
||||||
id: deleteConfirmDialog
|
|
||||||
title: i18nc("@title", "Delete Application")
|
|
||||||
subtitle: i18nc("@info", "Are you sure you want to delete this application? This cannot be undone.")
|
|
||||||
standardButtons: QQC2.Dialog.Cancel
|
|
||||||
|
|
||||||
customFooterActions: [
|
|
||||||
Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Delete")
|
|
||||||
icon.name: "edit-delete"
|
|
||||||
onTriggered: {
|
|
||||||
root.jobsModel.removeJob(root.editingJobId);
|
|
||||||
deleteConfirmDialog.close();
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
// Styled after KDE System Settings' Audio page (plasma-pa's kcm/ui/main.qml):
|
||||||
|
// Kirigami.ListSectionHeader per category, flat rows indented under the
|
||||||
|
// header and separated by Kirigami.Separator, no card/border around them.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kitemmodels as KItemModels
|
||||||
|
import org.kde.kirigamiaddons.dateandtime as DateTime
|
||||||
|
import io.github.toservetheking.Kareer
|
||||||
|
|
||||||
|
Kirigami.ScrollablePage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property JobsModel jobsModel
|
||||||
|
property int editingJobId: -1
|
||||||
|
|
||||||
|
title: root.editingJobId < 0 ? i18nc("@title", "Add Application") : i18nc("@title", "Edit Application")
|
||||||
|
|
||||||
|
signal done()
|
||||||
|
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
// No topPadding: Kirigami.ListSectionHeader (the first thing on the
|
||||||
|
// page) already carries its own top padding, and stacking ours on top
|
||||||
|
// of that left an oversized gap above "Company", the first category.
|
||||||
|
bottomPadding: Kirigami.Units.gridUnit
|
||||||
|
|
||||||
|
JobEditModel {
|
||||||
|
id: editModel
|
||||||
|
jobsModel: root.jobsModel
|
||||||
|
editingJobId: root.editingJobId
|
||||||
|
}
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:button", "Cancel")
|
||||||
|
icon.name: "dialog-cancel"
|
||||||
|
onTriggered: root.done()
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:button", "Delete")
|
||||||
|
icon.name: "edit-delete"
|
||||||
|
visible: root.editingJobId >= 0
|
||||||
|
onTriggered: deleteConfirmDialog.open()
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:button", "Save")
|
||||||
|
icon.name: "document-save"
|
||||||
|
onTriggered: {
|
||||||
|
if (editModel.save()) {
|
||||||
|
root.done();
|
||||||
|
} else {
|
||||||
|
errorLabel.text = editModel.lastError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Kirigami.InlineMessage {
|
||||||
|
id: errorLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
|
type: Kirigami.MessageType.Error
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: editModel.categories
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: section
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Kirigami.ListSectionHeader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: section.modelData.title
|
||||||
|
}
|
||||||
|
|
||||||
|
KItemModels.KSortFilterProxyModel {
|
||||||
|
id: catModel
|
||||||
|
sourceModel: editModel
|
||||||
|
filterRoleName: "categoryId"
|
||||||
|
// No category id is a prefix of another, so this is an exact match.
|
||||||
|
filterString: section.modelData.id
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: rowRepeater
|
||||||
|
model: catModel
|
||||||
|
|
||||||
|
delegate: DelegateChooser {
|
||||||
|
role: "rowType"
|
||||||
|
|
||||||
|
// Text
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: 0
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: textD
|
||||||
|
required property int index
|
||||||
|
required property string label
|
||||||
|
required property string value
|
||||||
|
required property string placeholder
|
||||||
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: textD.label
|
||||||
|
}
|
||||||
|
QQC2.TextField {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: textD.value
|
||||||
|
placeholderText: textD.placeholder
|
||||||
|
onTextEdited: editModel.setValue(textD.sourceRow, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: textD.index !== rowRepeater.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo box
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: 1
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: comboD
|
||||||
|
required property int index
|
||||||
|
required property string label
|
||||||
|
required property string value
|
||||||
|
required property var comboOptions
|
||||||
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: comboD.label
|
||||||
|
}
|
||||||
|
QQC2.ComboBox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: comboD.comboOptions
|
||||||
|
currentIndex: comboD.comboOptions.indexOf(comboD.value)
|
||||||
|
onActivated: editModel.setValue(comboD.sourceRow, currentText)
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: comboD.index !== rowRepeater.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spin box
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: 2
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: spinD
|
||||||
|
required property int index
|
||||||
|
required property string label
|
||||||
|
required property var model
|
||||||
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: spinD.label
|
||||||
|
}
|
||||||
|
QQC2.SpinBox {
|
||||||
|
id: spinBox
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: spinD.model.spinMin
|
||||||
|
to: spinD.model.spinMax
|
||||||
|
stepSize: 1000
|
||||||
|
// One-time init, not a persistent binding: this also
|
||||||
|
// writes back on user edits, so binding "value" live to
|
||||||
|
// spinD.model.value would be a binding loop.
|
||||||
|
Component.onCompleted: value = spinD.model.value
|
||||||
|
onValueChanged: editModel.setValue(spinD.sourceRow, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: spinD.index !== rowRepeater.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: 3
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: dateD
|
||||||
|
required property int index
|
||||||
|
required property string label
|
||||||
|
required property var value
|
||||||
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: dateD.label
|
||||||
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Qt.formatDate(dateD.value, Qt.ISODate)
|
||||||
|
icon.name: "view-calendar-day"
|
||||||
|
onClicked: {
|
||||||
|
datePopup.value = dateD.value;
|
||||||
|
datePopup.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime.DatePopup {
|
||||||
|
id: datePopup
|
||||||
|
onAccepted: editModel.setValue(dateD.sourceRow, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: dateD.index !== rowRepeater.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text area
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: 4
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: areaD
|
||||||
|
required property int index
|
||||||
|
required property string label
|
||||||
|
required property string value
|
||||||
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: areaD.label
|
||||||
|
}
|
||||||
|
QQC2.TextArea {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 5
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
text: areaD.value
|
||||||
|
onTextChanged: editModel.setValue(areaD.sourceRow, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: areaD.index !== rowRepeater.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: deleteConfirmDialog
|
||||||
|
title: i18nc("@title", "Delete Application")
|
||||||
|
subtitle: i18nc("@info", "Are you sure you want to delete this application? This cannot be undone.")
|
||||||
|
standardButtons: QQC2.Dialog.Cancel
|
||||||
|
|
||||||
|
customFooterActions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:button", "Delete")
|
||||||
|
icon.name: "edit-delete"
|
||||||
|
onTriggered: {
|
||||||
|
editModel.deleteJob();
|
||||||
|
deleteConfirmDialog.close();
|
||||||
|
root.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
// Sidebar/detail two-column layout: a search-filtered list of applications
|
||||||
|
// with a RoundedItemDelegate + SubtitleContentItem delegate, where the
|
||||||
|
// currently-open entry stays highlighted.
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
@@ -12,7 +20,9 @@ Kirigami.ScrollablePage {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property JobsModel jobsModel
|
required property JobsModel jobsModel
|
||||||
required property var editDialog
|
property int currentJobId: -1
|
||||||
|
|
||||||
|
signal editRequested(int jobId)
|
||||||
|
|
||||||
title: i18nc("@title", "Applications")
|
title: i18nc("@title", "Applications")
|
||||||
|
|
||||||
@@ -21,14 +31,6 @@ Kirigami.ScrollablePage {
|
|||||||
onTextChanged: filteredJobs.filterString = text
|
onTextChanged: filteredJobs.filterString = text
|
||||||
}
|
}
|
||||||
|
|
||||||
actions: [
|
|
||||||
Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Add Application")
|
|
||||||
icon.name: "list-add"
|
|
||||||
onTriggered: root.editDialog.openForAdd()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
KItemModels.KSortFilterProxyModel {
|
KItemModels.KSortFilterProxyModel {
|
||||||
id: filteredJobs
|
id: filteredJobs
|
||||||
sourceModel: root.jobsModel
|
sourceModel: root.jobsModel
|
||||||
@@ -49,32 +51,17 @@ Kirigami.ScrollablePage {
|
|||||||
required property string company
|
required property string company
|
||||||
required property string title
|
required property string title
|
||||||
required property string stage
|
required property string stage
|
||||||
required property var dateApplied
|
|
||||||
required property int salaryMin
|
|
||||||
required property int salaryMax
|
|
||||||
required property string currency
|
|
||||||
|
|
||||||
text: jobDelegate.company
|
text: jobDelegate.company
|
||||||
|
icon.source: "network-workgroup-symbolic"
|
||||||
|
highlighted: root.currentJobId === jobDelegate.jobId
|
||||||
|
|
||||||
contentItem: Delegates.SubtitleContentItem {
|
contentItem: Delegates.SubtitleContentItem {
|
||||||
itemDelegate: jobDelegate
|
itemDelegate: jobDelegate
|
||||||
subtitle: {
|
subtitle: jobDelegate.title + " · " + jobDelegate.stage
|
||||||
const parts = [jobDelegate.title, jobDelegate.stage];
|
|
||||||
if (jobDelegate.dateApplied) {
|
|
||||||
parts.push(Qt.formatDate(jobDelegate.dateApplied, "yyyy-MM-dd"));
|
|
||||||
}
|
|
||||||
if (jobDelegate.salaryMin >= 0 || jobDelegate.salaryMax >= 0) {
|
|
||||||
let salary = jobDelegate.currency + " ";
|
|
||||||
salary += jobDelegate.salaryMin >= 0 ? jobDelegate.salaryMin : "?";
|
|
||||||
salary += "–";
|
|
||||||
salary += jobDelegate.salaryMax >= 0 ? jobDelegate.salaryMax : "?";
|
|
||||||
parts.push(salary);
|
|
||||||
}
|
|
||||||
return parts.join(" · ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: root.editDialog.openForEdit(jobDelegate.jobId)
|
onClicked: root.editRequested(jobDelegate.jobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.PlaceholderMessage {
|
Kirigami.PlaceholderMessage {
|
||||||
@@ -83,7 +70,7 @@ Kirigami.ScrollablePage {
|
|||||||
visible: jobList.count === 0
|
visible: jobList.count === 0
|
||||||
icon.name: "office-address-book-symbolic"
|
icon.name: "office-address-book-symbolic"
|
||||||
text: i18n("No applications yet")
|
text: i18n("No applications yet")
|
||||||
explanation: i18n("Use the Add Application button to log your first one.")
|
explanation: i18n("Use the Add Application button on the dashboard to log your first one.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -12,8 +17,18 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
required property JobsModel jobsModel
|
required property JobsModel jobsModel
|
||||||
|
|
||||||
|
signal addRequested()
|
||||||
|
|
||||||
title: i18nc("@title", "Dashboard")
|
title: i18nc("@title", "Dashboard")
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:button", "Add Application")
|
||||||
|
icon.name: "list-add"
|
||||||
|
onTriggered: root.addRequested()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
StatsModel {
|
StatsModel {
|
||||||
id: statsModel
|
id: statsModel
|
||||||
}
|
}
|
||||||
@@ -57,7 +72,7 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
text: statCard.modelData.label
|
text: statCard.modelData.label
|
||||||
opacity: 0.7
|
color: Kirigami.Theme.disabledTextColor
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
import io.github.toservetheking.Kareer
|
import io.github.toservetheking.Kareer
|
||||||
|
|
||||||
Kirigami.ApplicationWindow {
|
Kirigami.ApplicationWindow {
|
||||||
@@ -19,22 +25,73 @@ Kirigami.ApplicationWindow {
|
|||||||
id: jobsModel
|
id: jobsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationEditDialog {
|
// The job currently open in the edit form, so the sidebar can keep it highlighted.
|
||||||
id: editDialog
|
property int currentJobId: -1
|
||||||
jobsModel: jobsModel
|
|
||||||
}
|
|
||||||
|
|
||||||
pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 22
|
pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 16
|
||||||
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||||
|
|
||||||
// Two static columns (applications + dashboard) - no dynamic page pushing.
|
globalDrawer: Kirigami.GlobalDrawer {
|
||||||
|
isMenu: true
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:inmenu", "Preferences…")
|
||||||
|
icon.name: "configure"
|
||||||
|
onTriggered: root.pageStack.pushDialogLayer(settingsPageComponent, {
|
||||||
|
width: root.width
|
||||||
|
}, {
|
||||||
|
width: Kirigami.Units.gridUnit * 24,
|
||||||
|
height: Kirigami.Units.gridUnit * 20,
|
||||||
|
modality: Qt.NonModal
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:inmenu", "About %1", root.title)
|
||||||
|
icon.name: "help-about"
|
||||||
|
onTriggered: root.pageStack.pushDialogLayer(aboutPageComponent, {
|
||||||
|
width: root.width
|
||||||
|
}, {
|
||||||
|
width: Kirigami.Units.gridUnit * 30,
|
||||||
|
height: Kirigami.Units.gridUnit * 30,
|
||||||
|
modality: Qt.NonModal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: aboutPageComponent
|
||||||
|
FormCard.AboutPage {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: settingsPageComponent
|
||||||
|
SettingsPage {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two static columns: the applications list (sidebar) and the dashboard.
|
||||||
|
// "Add Application" (and clicking a row) replaces just the dashboard
|
||||||
|
// column with the edit form - the sidebar and its list are untouched.
|
||||||
pageStack.initialPage: [applicationsComponent, dashboardComponent]
|
pageStack.initialPage: [applicationsComponent, dashboardComponent]
|
||||||
|
|
||||||
|
function showDashboard(): void {
|
||||||
|
root.currentJobId = -1;
|
||||||
|
root.pageStack.currentIndex = 1;
|
||||||
|
root.pageStack.replace(dashboardComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditPage(jobId: int): void {
|
||||||
|
root.currentJobId = jobId;
|
||||||
|
root.pageStack.currentIndex = 1;
|
||||||
|
root.pageStack.replace(editPageComponent, {editingJobId: jobId});
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: applicationsComponent
|
id: applicationsComponent
|
||||||
ApplicationsPage {
|
ApplicationsPage {
|
||||||
jobsModel: jobsModel
|
jobsModel: jobsModel
|
||||||
editDialog: editDialog
|
currentJobId: root.currentJobId
|
||||||
|
onEditRequested: jobId => root.showEditPage(jobId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +99,15 @@ Kirigami.ApplicationWindow {
|
|||||||
id: dashboardComponent
|
id: dashboardComponent
|
||||||
DashboardPage {
|
DashboardPage {
|
||||||
jobsModel: jobsModel
|
jobsModel: jobsModel
|
||||||
|
onAddRequested: root.showEditPage(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: editPageComponent
|
||||||
|
ApplicationEditPage {
|
||||||
|
jobsModel: jobsModel
|
||||||
|
onDone: root.showDashboard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
// Styled after KDE System Settings' Audio page (plasma-pa's kcm/ui/main.qml):
|
||||||
|
// Kirigami.ListSectionHeader per section, flat rows indented under the
|
||||||
|
// header, no card/border around them.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import io.github.toservetheking.Kareer
|
||||||
|
|
||||||
|
Kirigami.ScrollablePage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Preferences")
|
||||||
|
|
||||||
|
leftPadding: 0
|
||||||
|
rightPadding: 0
|
||||||
|
// No topPadding: Kirigami.ListSectionHeader (the first thing on the
|
||||||
|
// page) already carries its own top padding, and stacking ours on top
|
||||||
|
// of that left an oversized gap above "Appearance".
|
||||||
|
bottomPadding: Kirigami.Units.gridUnit
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: root.width
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Kirigami.ListSectionHeader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: i18nc("@title:group", "Appearance")
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: i18nc("@label:listbox", "Color scheme")
|
||||||
|
}
|
||||||
|
QQC2.ComboBox {
|
||||||
|
id: colorSchemeCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
model: AppColorScheme.colorSchemesModel
|
||||||
|
textRole: "display"
|
||||||
|
|
||||||
|
delegate: QQC2.ItemDelegate {
|
||||||
|
id: schemeDelegate
|
||||||
|
required property var model
|
||||||
|
width: colorSchemeCombo.width
|
||||||
|
|
||||||
|
icon.source: "image://colorScheme/" + schemeDelegate.model.display
|
||||||
|
icon.color: "transparent"
|
||||||
|
text: schemeDelegate.model.display
|
||||||
|
highlighted: schemeDelegate.model.display === AppColorScheme.activeColorSchemeName
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
AppColorScheme.activeColorSchemeName = schemeDelegate.model.display;
|
||||||
|
colorSchemeCombo.popup.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the closed-box label in sync without fighting the popup's own selection state.
|
||||||
|
displayText: AppColorScheme.activeColorSchemeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "sankeymodel.h"
|
#include "sankeymodel.h"
|
||||||
#include "jobstage.h"
|
#include "jobstage.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "jobsdatabase.h"
|
#include "jobsdatabase.h"
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#include "statsmodel.h"
|
#include "statsmodel.h"
|
||||||
#include "jobstage.h"
|
#include "jobstage.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "jobsdatabase.h"
|
#include "jobsdatabase.h"
|
||||||
|
|||||||