Add --db and let the user choose where the database lives
- `kareer --db <path>` works for the GUI and every subcommand, before or after the subcommand name, and creates the file if missing. main() strips it from argv before CLI/GUI routing; every parser declares it so it shows in --help. - Path precedence: --db > KAREER_DB_PATH > the location chosen in the GUI (kareerrc [Database] Path, read at startup for GUI and CLI alike) > $XDG_DATA_HOME/kareer/kareer.sqlite. - First GUI run (nothing forced, no file yet, or the configured file has gone missing): DatabaseSetupDialog offers the default location, a folder of the user's choice, or an existing database used in place. Until then JobsDatabase opens nothing. - Preferences gains a Database section: Move to... (copies, leaves the original), Open Existing..., Use Default Location. Refuses foreign SQLite files and unwritable ones. - Models call JobsDatabase::reopenIfPathChanged() on refresh, so switching databases needs no restart. - Link KF6::ConfigCore; add a QuickDialogs2 configure-time guard; add kconfig to the Arch dependencies and a note to the Flatpak manifest. - README documents --db, the precedence, and the first-run choice, and gains Fedora 44 build dependencies; CONTRIBUTING gets a dev recipe. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01BxDf7HqD1xPnsZP8wt3NTk
This commit is contained in:
@@ -36,12 +36,16 @@ find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS
|
|||||||
# QtQuick.Shapes runtime QML module (SankeyDiagram.qml); configure-time
|
# QtQuick.Shapes runtime QML module (SankeyDiagram.qml); configure-time
|
||||||
# guard only, nothing links against it.
|
# guard only, nothing links against it.
|
||||||
QuickShapesPrivate
|
QuickShapesPrivate
|
||||||
|
# QtQuick.Dialogs runtime QML module (DatabaseSetupDialog.qml,
|
||||||
|
# SettingsPage.qml); configure-time guard only, nothing links against it.
|
||||||
|
QuickDialogs2
|
||||||
QuickControls2
|
QuickControls2
|
||||||
Sql
|
Sql
|
||||||
Test
|
Test
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(KF6 ${REQUIRED_KF_VERSION} REQUIRED COMPONENTS
|
find_package(KF6 ${REQUIRED_KF_VERSION} REQUIRED COMPONENTS
|
||||||
|
Config
|
||||||
I18n
|
I18n
|
||||||
CoreAddons
|
CoreAddons
|
||||||
IconThemes
|
IconThemes
|
||||||
|
|||||||
@@ -16,6 +16,22 @@ Every GUI action is also available from the `kareer` CLI
|
|||||||
(`add|list|show|update|stage|delete|stats|stages`) — run `./build/bin/kareer
|
(`add|list|show|update|stage|delete|stats|stages`) — run `./build/bin/kareer
|
||||||
--help` to see the subcommands.
|
--help` to see the subcommands.
|
||||||
|
|
||||||
|
To develop against a throwaway database instead of your real one, pass
|
||||||
|
`--db` (the file is created if it doesn't exist); it works for the GUI and
|
||||||
|
every subcommand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build/bin/kareer --db build/dev.sqlite add --company Acme --title Engineer
|
||||||
|
./build/bin/kareer --db build/dev.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
To see the first-run "where should the database live?" dialog, start with
|
||||||
|
empty config and data directories:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
XDG_CONFIG_HOME=$(mktemp -d) XDG_DATA_HOME=$(mktemp -d) ./build/bin/kareer
|
||||||
|
```
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -58,9 +58,23 @@ kareer stages
|
|||||||
```
|
```
|
||||||
|
|
||||||
Run `kareer <command> --help` for the full option list of any
|
Run `kareer <command> --help` for the full option list of any
|
||||||
subcommand. Data lives in `$XDG_DATA_HOME/kareer/kareer.sqlite`
|
subcommand.
|
||||||
(under Flatpak, that's sandboxed to the app's own data directory); set
|
|
||||||
`KAREER_DB_PATH` to point at a different file.
|
### Where the data lives
|
||||||
|
|
||||||
|
On first launch the GUI asks where to keep the database: the default
|
||||||
|
location, a folder you choose (a synced folder, say), or an existing
|
||||||
|
`kareer.sqlite` you already have, used where it is. You can move or switch
|
||||||
|
it later under Preferences. The CLI uses the same file.
|
||||||
|
|
||||||
|
The database file is picked in this order:
|
||||||
|
|
||||||
|
1. `--db <path>` on the command line (GUI or any subcommand; the file is
|
||||||
|
created if it doesn't exist): `kareer --db ~/test.sqlite list`
|
||||||
|
2. The `KAREER_DB_PATH` environment variable
|
||||||
|
3. The location chosen in the GUI (stored in `kareerrc`)
|
||||||
|
4. `$XDG_DATA_HOME/kareer/kareer.sqlite` (under Flatpak, that's sandboxed to
|
||||||
|
the app's own data directory)
|
||||||
|
|
||||||
### Wiring up a resume-builder tool
|
### Wiring up a resume-builder tool
|
||||||
|
|
||||||
@@ -110,10 +124,23 @@ CMake toolchain. On Arch/CachyOS:
|
|||||||
```sh
|
```sh
|
||||||
sudo pacman -S --needed cmake ninja extra-cmake-modules base-devel \
|
sudo pacman -S --needed cmake ninja extra-cmake-modules base-devel \
|
||||||
qt6-base qt6-declarative vulkan-headers kirigami kirigami-addons \
|
qt6-base qt6-declarative vulkan-headers kirigami kirigami-addons \
|
||||||
ki18n kcoreaddons kiconthemes kcrash kitemmodels \
|
ki18n kconfig kcoreaddons kiconthemes kcrash kitemmodels \
|
||||||
kcolorscheme qqc2-desktop-style
|
kcolorscheme qqc2-desktop-style
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Fedora 44:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo dnf install cmake ninja-build extra-cmake-modules gcc-c++ \
|
||||||
|
qt6-qtbase-devel qt6-qtbase-private-devel \
|
||||||
|
qt6-qtdeclarative-devel qt6-qtquickcontrols2-devel \
|
||||||
|
vulkan-headers kf6-kirigami-devel \
|
||||||
|
kf6-kirigami-addons-devel kf6-ki18n-devel kf6-kcoreaddons-devel \
|
||||||
|
kf6-kconfig-devel \
|
||||||
|
kf6-kiconthemes-devel kf6-kcrash-devel kf6-kitemmodels-devel \
|
||||||
|
kf6-kcolorscheme-devel qqc2-desktop-style
|
||||||
|
```
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
Vendored
+1
@@ -12,6 +12,7 @@ license=('GPL-3.0-or-later')
|
|||||||
depends=(
|
depends=(
|
||||||
hicolor-icon-theme
|
hicolor-icon-theme
|
||||||
kcolorscheme
|
kcolorscheme
|
||||||
|
kconfig
|
||||||
kcoreaddons
|
kcoreaddons
|
||||||
kcrash
|
kcrash
|
||||||
ki18n
|
ki18n
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ runtime-version: '6.10'
|
|||||||
sdk: org.kde.Sdk
|
sdk: org.kde.Sdk
|
||||||
command: kareer
|
command: kareer
|
||||||
|
|
||||||
# A job application tracker: no host filesystem, network, or portal access is
|
# A job application tracker: no host filesystem or network access is needed.
|
||||||
# needed. Everything it stores lives in the app's own XDG data directory,
|
# By default everything lives in the app's own XDG data directory, which
|
||||||
# which Flatpak grants by default.
|
# Flatpak grants. A database the user places elsewhere (first-run dialog or
|
||||||
|
# Preferences) is reached through the file chooser portal instead of a
|
||||||
|
# --filesystem permission; folders are picked for new/moved databases so
|
||||||
|
# SQLite can create its journal next to the file.
|
||||||
finish-args:
|
finish-args:
|
||||||
- --share=ipc
|
- --share=ipc
|
||||||
- --socket=fallback-x11
|
- --socket=fallback-x11
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ qt_add_qml_module(kareer
|
|||||||
qml/DashboardPage.qml
|
qml/DashboardPage.qml
|
||||||
qml/SankeyDiagram.qml
|
qml/SankeyDiagram.qml
|
||||||
qml/SettingsPage.qml
|
qml/SettingsPage.qml
|
||||||
|
qml/DatabaseSetupDialog.qml
|
||||||
SOURCES
|
SOURCES
|
||||||
jobsmodel.cpp
|
jobsmodel.cpp
|
||||||
jobsmodel.h
|
jobsmodel.h
|
||||||
@@ -32,6 +33,8 @@ qt_add_qml_module(kareer
|
|||||||
jobeditmodel.h
|
jobeditmodel.h
|
||||||
appcolorscheme.cpp
|
appcolorscheme.cpp
|
||||||
appcolorscheme.h
|
appcolorscheme.h
|
||||||
|
databaselocation.cpp
|
||||||
|
databaselocation.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(kareer PRIVATE ${CMAKE_BINARY_DIR})
|
target_include_directories(kareer PRIVATE ${CMAKE_BINARY_DIR})
|
||||||
@@ -44,6 +47,7 @@ target_link_libraries(kareer PRIVATE
|
|||||||
Qt6::Quick
|
Qt6::Quick
|
||||||
Qt6::QuickControls2
|
Qt6::QuickControls2
|
||||||
Qt6::Sql
|
Qt6::Sql
|
||||||
|
KF6::ConfigCore
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
KF6::I18nQml
|
KF6::I18nQml
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
|
|||||||
+17
-1
@@ -26,6 +26,13 @@ using namespace Qt::Literals::StringLiterals;
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/// --db is applied and stripped in main() before any parser runs; declaring
|
||||||
|
/// it here only documents it in each subcommand's --help.
|
||||||
|
void addDbOption(QCommandLineParser &parser)
|
||||||
|
{
|
||||||
|
parser.addOption({u"db"_s, u"Use this database file instead of the configured one"_s, u"path"_s});
|
||||||
|
}
|
||||||
|
|
||||||
QJsonValue optionalInt(int value)
|
QJsonValue optionalInt(int value)
|
||||||
{
|
{
|
||||||
return value < 0 ? QJsonValue() : QJsonValue(value);
|
return value < 0 ? QJsonValue() : QJsonValue(value);
|
||||||
@@ -159,6 +166,7 @@ int runAdd(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"Add a new job application"_s);
|
parser.setApplicationDescription(u"Add a new job application"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
addCommonJobOptions(parser);
|
addCommonJobOptions(parser);
|
||||||
parser.process(QStringList{program} + args);
|
parser.process(QStringList{program} + args);
|
||||||
|
|
||||||
@@ -225,6 +233,7 @@ int runList(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"List job applications"_s);
|
parser.setApplicationDescription(u"List job applications"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
parser.addOption({u"stage"_s, u"Filter by stage"_s, u"stage"_s});
|
parser.addOption({u"stage"_s, u"Filter by stage"_s, u"stage"_s});
|
||||||
parser.addOption({u"company"_s, u"Filter by company (substring match)"_s, u"text"_s});
|
parser.addOption({u"company"_s, u"Filter by company (substring match)"_s, u"text"_s});
|
||||||
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
||||||
@@ -272,6 +281,7 @@ int runShow(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"Show one job application"_s);
|
parser.setApplicationDescription(u"Show one job application"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
||||||
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
||||||
parser.process(QStringList{program} + args);
|
parser.process(QStringList{program} + args);
|
||||||
@@ -310,6 +320,7 @@ int runUpdate(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"Update fields on an existing application"_s);
|
parser.setApplicationDescription(u"Update fields on an existing application"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
addCommonJobOptions(parser);
|
addCommonJobOptions(parser);
|
||||||
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
||||||
parser.process(QStringList{program} + args);
|
parser.process(QStringList{program} + args);
|
||||||
@@ -410,6 +421,7 @@ int runStage(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"Move an application to a new stage"_s);
|
parser.setApplicationDescription(u"Move an application to a new stage"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
||||||
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
||||||
parser.addPositionalArgument(u"stage"_s, u"New stage: %1"_s.arg(JobStage::canonicalStages().join(u", "_s)));
|
parser.addPositionalArgument(u"stage"_s, u"New stage: %1"_s.arg(JobStage::canonicalStages().join(u", "_s)));
|
||||||
@@ -453,6 +465,7 @@ int runDelete(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"Delete an application"_s);
|
parser.setApplicationDescription(u"Delete an application"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
parser.addOption({u"yes"_s, u"Confirm deletion"_s});
|
parser.addOption({u"yes"_s, u"Confirm deletion"_s});
|
||||||
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
|
||||||
parser.process(QStringList{program} + args);
|
parser.process(QStringList{program} + args);
|
||||||
@@ -490,6 +503,7 @@ int runStats(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"Summary statistics across all applications"_s);
|
parser.setApplicationDescription(u"Summary statistics across all applications"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
||||||
parser.process(QStringList{program} + args);
|
parser.process(QStringList{program} + args);
|
||||||
|
|
||||||
@@ -531,6 +545,7 @@ int runStages(const QString &program, const QStringList &args)
|
|||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(u"List the canonical pipeline stages"_s);
|
parser.setApplicationDescription(u"List the canonical pipeline stages"_s);
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
addDbOption(parser);
|
||||||
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
|
||||||
parser.process(QStringList{program} + args);
|
parser.process(QStringList{program} + args);
|
||||||
|
|
||||||
@@ -557,7 +572,8 @@ int runHelp()
|
|||||||
<< u" list List job applications\n"_s << u" show Show one job application\n"_s << u" update Update fields on an existing application\n"_s
|
<< u" list List job applications\n"_s << u" show Show one job application\n"_s << u" update Update fields on an existing application\n"_s
|
||||||
<< u" stage Move an application to a new stage\n"_s << u" delete Delete an application\n"_s << u" stats Summary statistics\n"_s
|
<< u" stage Move an application to a new stage\n"_s << u" delete Delete an application\n"_s << u" stats Summary statistics\n"_s
|
||||||
<< u" stages List the canonical pipeline stages\n\n"_s << u"Run 'kareer <command> --help' for the options of a specific command.\n"_s
|
<< u" stages List the canonical pipeline stages\n\n"_s << u"Run 'kareer <command> --help' for the options of a specific command.\n"_s
|
||||||
<< u"Running kareer with no command (or an unrecognized one) starts the GUI.\n"_s;
|
<< u"Running kareer with no command (or an unrecognized one) starts the GUI.\n\n"_s << u"Global options:\n"_s
|
||||||
|
<< u" --db <path> Use this database file (created if missing) instead of the configured one\n"_s;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "databaselocation.h"
|
||||||
|
|
||||||
|
#include "jobsdatabase.h"
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr auto ConfigFile = "kareerrc";
|
||||||
|
constexpr auto ConfigGroup = "Database";
|
||||||
|
constexpr auto ConfigKey = "Path";
|
||||||
|
constexpr auto DatabaseFileName = "kareer.sqlite";
|
||||||
|
|
||||||
|
KConfigGroup databaseGroup()
|
||||||
|
{
|
||||||
|
return KConfigGroup(KSharedConfig::openConfig(QString::fromLatin1(ConfigFile)), QString::fromLatin1(ConfigGroup));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString folderFile(const QUrl &folder)
|
||||||
|
{
|
||||||
|
return QDir(folder.toLocalFile()).filePath(QString::fromLatin1(DatabaseFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseLocation::DatabaseLocation(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
if (JobsDatabase::selectionPending() && !JobsDatabase::configuredPath().isEmpty()) {
|
||||||
|
m_missingPath = JobsDatabase::configuredPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseLocation::loadConfiguredPath()
|
||||||
|
{
|
||||||
|
JobsDatabase::setConfiguredPath(databaseGroup().readEntry(ConfigKey, QString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::needsSetup()
|
||||||
|
{
|
||||||
|
return !JobsDatabase::hasForcedPath() && !QFileInfo::exists(JobsDatabase::defaultPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DatabaseLocation::path() const
|
||||||
|
{
|
||||||
|
return JobsDatabase::selectionPending() ? QString() : JobsDatabase::defaultPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DatabaseLocation::standardPath() const
|
||||||
|
{
|
||||||
|
return JobsDatabase::standardPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::setupPending() const
|
||||||
|
{
|
||||||
|
return JobsDatabase::selectionPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DatabaseLocation::missingPath() const
|
||||||
|
{
|
||||||
|
return m_missingPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::overridden() const
|
||||||
|
{
|
||||||
|
return JobsDatabase::hasForcedPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DatabaseLocation::lastError() const
|
||||||
|
{
|
||||||
|
return m_lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DatabaseLocation::lastNotice() const
|
||||||
|
{
|
||||||
|
return m_lastNotice;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::useDefault()
|
||||||
|
{
|
||||||
|
return switchTo(QString(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::createIn(const QUrl &folder)
|
||||||
|
{
|
||||||
|
if (!folder.isLocalFile()) {
|
||||||
|
setMessages(i18n("Please choose a local folder."), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return switchTo(folderFile(folder), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::useFile(const QUrl &file)
|
||||||
|
{
|
||||||
|
if (!file.isLocalFile()) {
|
||||||
|
setMessages(i18n("Please choose a local file."), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return switchTo(file.toLocalFile(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::moveTo(const QUrl &folder)
|
||||||
|
{
|
||||||
|
if (!folder.isLocalFile()) {
|
||||||
|
setMessages(i18n("Please choose a local folder."), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QString source = JobsDatabase::defaultPath();
|
||||||
|
const QString target = folderFile(folder);
|
||||||
|
if (QFileInfo(source).canonicalFilePath() == QFileInfo(target).canonicalFilePath()) {
|
||||||
|
setMessages(i18n("The database is already in that folder."), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (QFileInfo::exists(target)) {
|
||||||
|
setMessages(i18n("%1 already exists. Choose another folder, or open that file instead.", target), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!QFile::copy(source, target)) {
|
||||||
|
setMessages(i18n("Could not copy the database to %1.", target), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!switchTo(target, true)) {
|
||||||
|
QFile::remove(target);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setMessages(QString(), i18n("Now using %1. The previous file at %2 was left in place.", target, source));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseLocation::clearMessages()
|
||||||
|
{
|
||||||
|
setMessages(QString(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseLocation::switchTo(const QString &path, bool mustExist)
|
||||||
|
{
|
||||||
|
const QString effective = path.isEmpty() ? JobsDatabase::standardPath() : path;
|
||||||
|
|
||||||
|
if (mustExist && !QFileInfo::exists(effective)) {
|
||||||
|
setMessages(i18n("%1 does not exist.", effective), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refuse to add Kareer's tables to some unrelated SQLite file.
|
||||||
|
if (QFileInfo::exists(effective)) {
|
||||||
|
const QString connection = u"kareer_probe"_s;
|
||||||
|
bool foreign = false;
|
||||||
|
{
|
||||||
|
QSqlDatabase probe = QSqlDatabase::addDatabase(u"QSQLITE"_s, connection);
|
||||||
|
probe.setDatabaseName(effective);
|
||||||
|
probe.setConnectOptions(u"QSQLITE_OPEN_READONLY"_s);
|
||||||
|
if (probe.open()) {
|
||||||
|
const QStringList tables = probe.tables();
|
||||||
|
foreign = !tables.isEmpty() && !tables.contains(u"jobs"_s);
|
||||||
|
}
|
||||||
|
probe.close();
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connection);
|
||||||
|
if (foreign) {
|
||||||
|
setMessages(i18n("%1 is not a Kareer database.", effective), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Opening creates the file and schema if needed; the write check
|
||||||
|
// catches files that can be read but not written (for example a
|
||||||
|
// single-file grant inside the Flatpak sandbox, where SQLite cannot
|
||||||
|
// create its journal next to the database).
|
||||||
|
JobsDatabase db(effective);
|
||||||
|
if (!db.isOpen() || !db.lastError().isEmpty() || !db.checkWritable()) {
|
||||||
|
setMessages(i18n("Could not use %1: %2", effective, db.lastError()), QString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KConfigGroup group = databaseGroup();
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
group.deleteEntry(ConfigKey);
|
||||||
|
} else {
|
||||||
|
group.writeEntry(ConfigKey, path);
|
||||||
|
}
|
||||||
|
group.sync();
|
||||||
|
|
||||||
|
JobsDatabase::setConfiguredPath(path);
|
||||||
|
JobsDatabase::setSelectionPending(false);
|
||||||
|
m_missingPath.clear();
|
||||||
|
setMessages(QString(), QString());
|
||||||
|
Q_EMIT changed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseLocation::setMessages(const QString &error, const QString ¬ice)
|
||||||
|
{
|
||||||
|
if (m_lastError == error && m_lastNotice == notice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_lastError = error;
|
||||||
|
m_lastNotice = notice;
|
||||||
|
Q_EMIT messageChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the database lives, as chosen by the user: the first-run dialog and
|
||||||
|
* the Preferences page drive this. The choice is stored in kareerrc
|
||||||
|
* ([Database] Path) and pushed into JobsDatabase::setConfiguredPath(); the
|
||||||
|
* --db option and KAREER_DB_PATH still win over it (see JobsDatabase::defaultPath()).
|
||||||
|
*
|
||||||
|
* Emits changed() after every switch; Main.qml reacts by refreshing the
|
||||||
|
* models, which reopen themselves via JobsDatabase::reopenIfPathChanged().
|
||||||
|
*/
|
||||||
|
class DatabaseLocation : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
/// The effective database file, or empty while the first-run choice is pending.
|
||||||
|
Q_PROPERTY(QString path READ path NOTIFY changed)
|
||||||
|
/// $XDG_DATA_HOME/kareer/kareer.sqlite.
|
||||||
|
Q_PROPERTY(QString standardPath READ standardPath CONSTANT)
|
||||||
|
Q_PROPERTY(bool setupPending READ setupPending NOTIFY changed)
|
||||||
|
/// The configured file that could not be found (so setup is being asked again), if any.
|
||||||
|
Q_PROPERTY(QString missingPath READ missingPath NOTIFY changed)
|
||||||
|
/// True when --db or KAREER_DB_PATH is in effect; the stored location is then ignored.
|
||||||
|
Q_PROPERTY(bool overridden READ overridden CONSTANT)
|
||||||
|
Q_PROPERTY(QString lastError READ lastError NOTIFY messageChanged)
|
||||||
|
Q_PROPERTY(QString lastNotice READ lastNotice NOTIFY messageChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DatabaseLocation(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/// Reads kareerrc into JobsDatabase::setConfiguredPath(). Call once at
|
||||||
|
/// startup, for both the GUI and the CLI, so they share one database.
|
||||||
|
static void loadConfiguredPath();
|
||||||
|
|
||||||
|
/// True when the GUI should ask where the database lives: nothing forces
|
||||||
|
/// a path and the file it would open does not exist yet.
|
||||||
|
static bool needsSetup();
|
||||||
|
|
||||||
|
QString path() const;
|
||||||
|
QString standardPath() const;
|
||||||
|
bool setupPending() const;
|
||||||
|
QString missingPath() const;
|
||||||
|
bool overridden() const;
|
||||||
|
QString lastError() const;
|
||||||
|
QString lastNotice() const;
|
||||||
|
|
||||||
|
/// Create (or reuse) the database at the standard location.
|
||||||
|
Q_INVOKABLE bool useDefault();
|
||||||
|
/// Create (or reuse) kareer.sqlite inside the chosen folder.
|
||||||
|
Q_INVOKABLE bool createIn(const QUrl &folder);
|
||||||
|
/// Use an existing database file where it is.
|
||||||
|
Q_INVOKABLE bool useFile(const QUrl &file);
|
||||||
|
/// Copy the current database into the chosen folder and switch to the
|
||||||
|
/// copy. The original file is left in place.
|
||||||
|
Q_INVOKABLE bool moveTo(const QUrl &folder);
|
||||||
|
|
||||||
|
Q_INVOKABLE void clearMessages();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void changed();
|
||||||
|
void messageChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Validates path, stores it (empty = standard location), and switches to it.
|
||||||
|
bool switchTo(const QString &path, bool mustExist);
|
||||||
|
void setMessages(const QString &error, const QString ¬ice);
|
||||||
|
|
||||||
|
QString m_missingPath;
|
||||||
|
QString m_lastError;
|
||||||
|
QString m_lastNotice;
|
||||||
|
};
|
||||||
+112
-6
@@ -22,6 +22,10 @@ namespace
|
|||||||
{
|
{
|
||||||
QAtomicInteger<int> s_connectionCounter{0};
|
QAtomicInteger<int> s_connectionCounter{0};
|
||||||
|
|
||||||
|
QString s_pathOverride;
|
||||||
|
QString s_configuredPath;
|
||||||
|
bool s_selectionPending = false;
|
||||||
|
|
||||||
QVariant salaryToVariant(int value)
|
QVariant salaryToVariant(int value)
|
||||||
{
|
{
|
||||||
if (value < 0) {
|
if (value < 0) {
|
||||||
@@ -38,6 +42,10 @@ int salaryFromVariant(const QVariant &value)
|
|||||||
|
|
||||||
JobsDatabase::JobsDatabase()
|
JobsDatabase::JobsDatabase()
|
||||||
{
|
{
|
||||||
|
if (s_selectionPending) {
|
||||||
|
m_lastError = u"No database selected"_s;
|
||||||
|
return;
|
||||||
|
}
|
||||||
init(defaultPath());
|
init(defaultPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +56,14 @@ JobsDatabase::JobsDatabase(const QString &path)
|
|||||||
|
|
||||||
JobsDatabase::~JobsDatabase()
|
JobsDatabase::~JobsDatabase()
|
||||||
{
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void JobsDatabase::close()
|
||||||
|
{
|
||||||
|
if (m_connectionName.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
|
QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
|
||||||
if (db.isValid()) {
|
if (db.isValid()) {
|
||||||
@@ -55,22 +71,103 @@ JobsDatabase::~JobsDatabase()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
QSqlDatabase::removeDatabase(m_connectionName);
|
QSqlDatabase::removeDatabase(m_connectionName);
|
||||||
|
m_connectionName.clear();
|
||||||
|
m_path.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString JobsDatabase::defaultPath()
|
QString JobsDatabase::defaultPath()
|
||||||
{
|
{
|
||||||
const QString overridePath = qEnvironmentVariable("KAREER_DB_PATH");
|
if (!s_pathOverride.isEmpty()) {
|
||||||
if (!overridePath.isEmpty()) {
|
return s_pathOverride;
|
||||||
return overridePath;
|
|
||||||
}
|
}
|
||||||
const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/kareer"_s;
|
const QString envPath = qEnvironmentVariable("KAREER_DB_PATH");
|
||||||
QDir().mkpath(dir);
|
if (!envPath.isEmpty()) {
|
||||||
return dir + u"/kareer.sqlite"_s;
|
return envPath;
|
||||||
|
}
|
||||||
|
if (!s_configuredPath.isEmpty()) {
|
||||||
|
return s_configuredPath;
|
||||||
|
}
|
||||||
|
return standardPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString JobsDatabase::standardPath()
|
||||||
|
{
|
||||||
|
return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/kareer/kareer.sqlite"_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JobsDatabase::setPathOverride(const QString &path)
|
||||||
|
{
|
||||||
|
s_pathOverride = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JobsDatabase::setConfiguredPath(const QString &path)
|
||||||
|
{
|
||||||
|
s_configuredPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString JobsDatabase::configuredPath()
|
||||||
|
{
|
||||||
|
return s_configuredPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JobsDatabase::hasForcedPath()
|
||||||
|
{
|
||||||
|
return !s_pathOverride.isEmpty() || !qEnvironmentVariableIsEmpty("KAREER_DB_PATH");
|
||||||
|
}
|
||||||
|
|
||||||
|
void JobsDatabase::setSelectionPending(bool pending)
|
||||||
|
{
|
||||||
|
s_selectionPending = pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JobsDatabase::selectionPending()
|
||||||
|
{
|
||||||
|
return s_selectionPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString JobsDatabase::path() const
|
||||||
|
{
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JobsDatabase::reopenIfPathChanged()
|
||||||
|
{
|
||||||
|
if (s_selectionPending) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QString wanted = defaultPath();
|
||||||
|
if (!m_path.isEmpty() && wanted == m_path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
m_lastError.clear();
|
||||||
|
init(wanted);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JobsDatabase::checkWritable()
|
||||||
|
{
|
||||||
|
if (!isOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||||
|
if (!query.exec(u"PRAGMA user_version"_s) || !query.next()) {
|
||||||
|
m_lastError = query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int version = query.value(0).toInt();
|
||||||
|
query.finish();
|
||||||
|
if (!query.exec(u"PRAGMA user_version = %1"_s.arg(version))) {
|
||||||
|
m_lastError = query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void JobsDatabase::init(const QString &path)
|
void JobsDatabase::init(const QString &path)
|
||||||
{
|
{
|
||||||
m_connectionName = u"kareer_conn_%1"_s.arg(s_connectionCounter.fetchAndAddRelaxed(1));
|
m_connectionName = u"kareer_conn_%1"_s.arg(s_connectionCounter.fetchAndAddRelaxed(1));
|
||||||
|
m_path = path;
|
||||||
|
|
||||||
QDir().mkpath(QFileInfo(path).absolutePath());
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||||
|
|
||||||
@@ -191,6 +288,9 @@ Job JobsDatabase::jobFromQuery(QSqlQuery &query) const
|
|||||||
QList<Job> JobsDatabase::allJobs() const
|
QList<Job> JobsDatabase::allJobs() const
|
||||||
{
|
{
|
||||||
QList<Job> jobs;
|
QList<Job> jobs;
|
||||||
|
if (!isOpen()) {
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||||
query.prepare(u"SELECT * FROM jobs ORDER BY date_applied DESC, id DESC"_s);
|
query.prepare(u"SELECT * FROM jobs ORDER BY date_applied DESC, id DESC"_s);
|
||||||
if (!query.exec()) {
|
if (!query.exec()) {
|
||||||
@@ -204,6 +304,9 @@ QList<Job> JobsDatabase::allJobs() const
|
|||||||
|
|
||||||
std::optional<Job> JobsDatabase::jobById(int id) const
|
std::optional<Job> JobsDatabase::jobById(int id) const
|
||||||
{
|
{
|
||||||
|
if (!isOpen()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||||
query.prepare(u"SELECT * FROM jobs WHERE id = :id"_s);
|
query.prepare(u"SELECT * FROM jobs WHERE id = :id"_s);
|
||||||
query.bindValue(u":id"_s, id);
|
query.bindValue(u":id"_s, id);
|
||||||
@@ -379,6 +482,9 @@ bool JobsDatabase::deleteJob(int id)
|
|||||||
QList<StageTransition> JobsDatabase::stageTransitions() const
|
QList<StageTransition> JobsDatabase::stageTransitions() const
|
||||||
{
|
{
|
||||||
QList<StageTransition> transitions;
|
QList<StageTransition> transitions;
|
||||||
|
if (!isOpen()) {
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||||
query.prepare(u"SELECT job_id, from_stage, to_stage, changed_at FROM stage_history ORDER BY changed_at ASC, id ASC"_s);
|
query.prepare(u"SELECT job_id, from_stage, to_stage, changed_at FROM stage_history ORDER BY changed_at ASC, id ASC"_s);
|
||||||
if (!query.exec()) {
|
if (!query.exec()) {
|
||||||
|
|||||||
+36
-2
@@ -31,10 +31,42 @@ public:
|
|||||||
JobsDatabase(const JobsDatabase &) = delete;
|
JobsDatabase(const JobsDatabase &) = delete;
|
||||||
JobsDatabase &operator=(const JobsDatabase &) = delete;
|
JobsDatabase &operator=(const JobsDatabase &) = delete;
|
||||||
|
|
||||||
/// Default location: $XDG_DATA_HOME/kareer/kareer.sqlite, overridable
|
/// The database file a default-constructed instance opens. Resolved in
|
||||||
/// with the KAREER_DB_PATH environment variable (used by autotests).
|
/// order: the --db override, the KAREER_DB_PATH environment variable
|
||||||
|
/// (used by autotests), the path configured in kareerrc, and finally
|
||||||
|
/// $XDG_DATA_HOME/kareer/kareer.sqlite.
|
||||||
static QString defaultPath();
|
static QString defaultPath();
|
||||||
|
|
||||||
|
/// $XDG_DATA_HOME/kareer/kareer.sqlite, ignoring every override.
|
||||||
|
static QString standardPath();
|
||||||
|
|
||||||
|
/// Set from the --db command-line option; wins over everything else.
|
||||||
|
static void setPathOverride(const QString &path);
|
||||||
|
|
||||||
|
/// The user's chosen location (kareerrc); empty means standardPath().
|
||||||
|
static void setConfiguredPath(const QString &path);
|
||||||
|
static QString configuredPath();
|
||||||
|
|
||||||
|
/// True when --db or KAREER_DB_PATH decides the path, so the configured
|
||||||
|
/// location has no effect.
|
||||||
|
static bool hasForcedPath();
|
||||||
|
|
||||||
|
/// While true (GUI first run, before the user has picked a location), a
|
||||||
|
/// default-constructed instance opens nothing and reports an error.
|
||||||
|
static void setSelectionPending(bool pending);
|
||||||
|
static bool selectionPending();
|
||||||
|
|
||||||
|
/// The file this instance opened, or empty if it opened nothing.
|
||||||
|
QString path() const;
|
||||||
|
|
||||||
|
/// Reopens against defaultPath() if that no longer matches path() (the
|
||||||
|
/// user picked another location). Returns true if it reopened.
|
||||||
|
bool reopenIfPathChanged();
|
||||||
|
|
||||||
|
/// Performs a harmless write (rewrites the header's user_version), so
|
||||||
|
/// callers can tell a read-only file apart from a usable one.
|
||||||
|
bool checkWritable();
|
||||||
|
|
||||||
bool isOpen() const;
|
bool isOpen() const;
|
||||||
QString lastError() const;
|
QString lastError() const;
|
||||||
|
|
||||||
@@ -59,9 +91,11 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void init(const QString &path);
|
void init(const QString &path);
|
||||||
|
void close();
|
||||||
bool migrate();
|
bool migrate();
|
||||||
Job jobFromQuery(class QSqlQuery &query) const;
|
Job jobFromQuery(class QSqlQuery &query) const;
|
||||||
|
|
||||||
QString m_connectionName;
|
QString m_connectionName;
|
||||||
|
QString m_path;
|
||||||
QString m_lastError;
|
QString m_lastError;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ JobsModel::JobsModel(QObject *parent)
|
|||||||
|
|
||||||
void JobsModel::refresh()
|
void JobsModel::refresh()
|
||||||
{
|
{
|
||||||
|
m_db.reopenIfPathChanged();
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_jobs = m_db.allJobs();
|
m_jobs = m_db.allJobs();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "kareer-version.h"
|
#include "kareer-version.h"
|
||||||
|
|
||||||
#include "clicommands.h"
|
#include "clicommands.h"
|
||||||
|
#include "databaselocation.h"
|
||||||
|
#include "jobsdatabase.h"
|
||||||
|
|
||||||
#include <KAboutData>
|
#include <KAboutData>
|
||||||
#include <KCrash>
|
#include <KCrash>
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
@@ -63,12 +66,50 @@ static void messageHandler(QtMsgType type, const QMessageLogContext &context, co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes "--db <path>" / "--db=<path>" from argv (anywhere on the command
|
||||||
|
/// line, for both the GUI and every subcommand) and applies it as the
|
||||||
|
/// database override. Stripping it up front keeps "kareer --db x list"
|
||||||
|
/// routing to the CLI. Returns false if --db is missing its value.
|
||||||
|
static bool takeDbOption(int &argc, char **argv)
|
||||||
|
{
|
||||||
|
int out = 1;
|
||||||
|
for (int in = 1; in < argc; ++in) {
|
||||||
|
const QString arg = QString::fromLocal8Bit(argv[in]);
|
||||||
|
QString value;
|
||||||
|
if (arg == u"--db"_s) {
|
||||||
|
if (in + 1 >= argc) {
|
||||||
|
fprintf(stderr, "kareer: --db requires a path\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = QString::fromLocal8Bit(argv[++in]);
|
||||||
|
} else if (arg.startsWith(u"--db="_s)) {
|
||||||
|
value = arg.mid(5);
|
||||||
|
} else {
|
||||||
|
argv[out++] = argv[in];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
fprintf(stderr, "kareer: --db requires a path\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JobsDatabase::setPathOverride(QFileInfo(value).absoluteFilePath());
|
||||||
|
}
|
||||||
|
argv[out] = nullptr;
|
||||||
|
argc = out;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
s_defaultMessageHandler = qInstallMessageHandler(messageHandler);
|
s_defaultMessageHandler = qInstallMessageHandler(messageHandler);
|
||||||
|
|
||||||
|
if (!takeDbOption(argc, argv)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (argc >= 2 && Cli::isSubcommand(QString::fromLocal8Bit(argv[1]))) {
|
if (argc >= 2 && Cli::isSubcommand(QString::fromLocal8Bit(argv[1]))) {
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
|
DatabaseLocation::loadConfiguredPath();
|
||||||
return Cli::run(app);
|
return Cli::run(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +140,16 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
aboutData.setupCommandLine(&parser);
|
aboutData.setupCommandLine(&parser);
|
||||||
|
// Handled (and stripped) by takeDbOption(); declared here for --help.
|
||||||
|
parser.addOption(QCommandLineOption(u"db"_s, i18n("Use this database file instead of the configured one."), i18n("path")));
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
aboutData.processCommandLine(&parser);
|
aboutData.processCommandLine(&parser);
|
||||||
|
|
||||||
|
DatabaseLocation::loadConfiguredPath();
|
||||||
|
// First run (or the configured file has gone missing): open nothing until
|
||||||
|
// the user picks a location in DatabaseSetupDialog. The CLI never waits.
|
||||||
|
JobsDatabase::setSelectionPending(DatabaseLocation::needsSetup());
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
KLocalization::setupLocalizedContext(&engine);
|
KLocalization::setupLocalizedContext(&engine);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
// First-run choice of where the database lives. Shown by Main.qml while
|
||||||
|
// DatabaseLocation.setupPending is true; it cannot be dismissed until a
|
||||||
|
// location has been picked, since nothing can be saved before then.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Dialogs
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
import io.github.toservetheking.Kareer
|
||||||
|
|
||||||
|
Kirigami.Dialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Welcome to Kareer")
|
||||||
|
showCloseButton: false
|
||||||
|
closePolicy: QQC2.Popup.NoAutoClose
|
||||||
|
modal: true
|
||||||
|
preferredWidth: Kirigami.Units.gridUnit * 26
|
||||||
|
standardButtons: Kirigami.Dialog.NoButton
|
||||||
|
|
||||||
|
onOpened: DatabaseLocation.clearMessages()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Kirigami.Units.largeSpacing
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: DatabaseLocation.missingPath.length > 0
|
||||||
|
? xi18nc("@info", "The database at <filename>%1</filename> could not be found. Choose where Kareer should keep your applications.", DatabaseLocation.missingPath)
|
||||||
|
: i18nc("@info", "Choose where Kareer should keep your applications.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.InlineMessage {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
|
type: Kirigami.MessageType.Error
|
||||||
|
text: DatabaseLocation.lastError
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
icon.name: "document-new"
|
||||||
|
text: i18nc("@action:button", "Create in the default location")
|
||||||
|
description: DatabaseLocation.standardPath
|
||||||
|
onClicked: {
|
||||||
|
if (DatabaseLocation.useDefault()) {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
icon.name: "folder-new"
|
||||||
|
text: i18nc("@action:button", "Choose a folder…")
|
||||||
|
description: i18nc("@info", "Create a new database in a folder of your choice, for example a synced folder.")
|
||||||
|
onClicked: folderDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
icon.name: "document-open"
|
||||||
|
text: i18nc("@action:button", "Open an existing database…")
|
||||||
|
description: i18nc("@info", "Use a kareer.sqlite file you already have, where it is.")
|
||||||
|
onClicked: fileDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderDialog {
|
||||||
|
id: folderDialog
|
||||||
|
title: i18nc("@title:window", "Choose a Folder for the Database")
|
||||||
|
onAccepted: {
|
||||||
|
if (DatabaseLocation.createIn(selectedFolder)) {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: fileDialog
|
||||||
|
title: i18nc("@title:window", "Open Database")
|
||||||
|
fileMode: FileDialog.OpenFile
|
||||||
|
nameFilters: [i18nc("@item:inlistbox", "Kareer databases (*.sqlite)"), i18nc("@item:inlistbox", "All files (*)")]
|
||||||
|
onAccepted: {
|
||||||
|
if (DatabaseLocation.useFile(selectedFile)) {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,27 @@ Kirigami.ApplicationWindow {
|
|||||||
id: jobsModel
|
id: jobsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The database moved (first-run choice or Preferences): reload the list,
|
||||||
|
// which cascades to the dashboard via countChanged, and leave any open
|
||||||
|
// edit form, since its job id belonged to the previous database.
|
||||||
|
Connections {
|
||||||
|
target: DatabaseLocation
|
||||||
|
function onChanged(): void {
|
||||||
|
jobsModel.refresh();
|
||||||
|
root.showDashboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseSetupDialog {
|
||||||
|
id: setupDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (DatabaseLocation.setupPending) {
|
||||||
|
setupDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The job currently open in the edit form, so the sidebar can keep it highlighted.
|
// The job currently open in the edit form, so the sidebar can keep it highlighted.
|
||||||
property int currentJobId: -1
|
property int currentJobId: -1
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pragma ComponentBehavior: Bound
|
|||||||
// header, no card/border around them.
|
// header, no card/border around them.
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Dialogs
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import io.github.toservetheking.Kareer
|
import io.github.toservetheking.Kareer
|
||||||
@@ -75,5 +76,94 @@ Kirigami.ScrollablePage {
|
|||||||
displayText: AppColorScheme.activeColorSchemeName
|
displayText: AppColorScheme.activeColorSchemeName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.ListSectionHeader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: i18nc("@title:group", "Database")
|
||||||
|
}
|
||||||
|
|
||||||
|
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", "Location")
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: DatabaseLocation.path
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.InlineMessage {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
type: Kirigami.MessageType.Information
|
||||||
|
text: i18nc("@info", "Set by the --db option or the KAREER_DB_PATH environment variable, so it can't be changed here.")
|
||||||
|
visible: DatabaseLocation.overridden
|
||||||
|
}
|
||||||
|
Kirigami.InlineMessage {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
type: Kirigami.MessageType.Error
|
||||||
|
text: DatabaseLocation.lastError
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
Kirigami.InlineMessage {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
type: Kirigami.MessageType.Positive
|
||||||
|
text: DatabaseLocation.lastNotice
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
enabled: !DatabaseLocation.overridden
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "folder-move"
|
||||||
|
text: i18nc("@action:button", "Move to…")
|
||||||
|
QQC2.ToolTip.text: i18nc("@info:tooltip", "Copy the database into another folder and use the copy from now on. The current file is left in place.")
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
onClicked: moveFolderDialog.open()
|
||||||
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "document-open"
|
||||||
|
text: i18nc("@action:button", "Open Existing…")
|
||||||
|
onClicked: openFileDialog.open()
|
||||||
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "edit-reset"
|
||||||
|
text: i18nc("@action:button", "Use Default Location")
|
||||||
|
visible: DatabaseLocation.path !== DatabaseLocation.standardPath
|
||||||
|
QQC2.ToolTip.text: xi18nc("@info:tooltip", "Switch to <filename>%1</filename>. Nothing is copied.", DatabaseLocation.standardPath)
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
onClicked: DatabaseLocation.useDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: DatabaseLocation.clearMessages()
|
||||||
|
|
||||||
|
FolderDialog {
|
||||||
|
id: moveFolderDialog
|
||||||
|
title: i18nc("@title:window", "Move Database To")
|
||||||
|
onAccepted: DatabaseLocation.moveTo(selectedFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: openFileDialog
|
||||||
|
title: i18nc("@title:window", "Open Database")
|
||||||
|
fileMode: FileDialog.OpenFile
|
||||||
|
nameFilters: [i18nc("@item:inlistbox", "Kareer databases (*.sqlite)"), i18nc("@item:inlistbox", "All files (*)")]
|
||||||
|
onAccepted: DatabaseLocation.useFile(selectedFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ StatsModel::StatsModel(QObject *parent)
|
|||||||
|
|
||||||
void StatsModel::refresh()
|
void StatsModel::refresh()
|
||||||
{
|
{
|
||||||
|
m_db.reopenIfPathChanged();
|
||||||
m_jobs = m_db.allJobs();
|
m_jobs = m_db.allJobs();
|
||||||
Q_EMIT changed();
|
Q_EMIT changed();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user