665 lines
16 KiB
C++
665 lines
16 KiB
C++
/*
|
|
* Copyright 2012-2018 Morgan McMillian <gilag@monkeystew.com>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "TaskModel.h"
|
|
|
|
TaskModel::TaskModel(QObject* parent) {
|
|
setParent(parent);
|
|
|
|
QSettings settings;
|
|
|
|
mOffline = false;
|
|
mDelete = -1;
|
|
|
|
ts_settings = new QSettings("Morgan McMillian", "RenamedTodo");
|
|
|
|
toast = new SystemToast(this);
|
|
|
|
mSort = settings.value("sort").toInt();
|
|
if (!mSort) mSort = PRIORITY;
|
|
|
|
QString fpath = settings.value("path").toString();
|
|
if (!fpath.isNull()) {
|
|
localfile.setPath(fpath);
|
|
}
|
|
|
|
regxComp.setPattern("^x ");
|
|
regxPri.setPattern("\\(([A-Z])\\) ");
|
|
regxDate.setPattern("([0-9]{4}-[0-9]{2}-[0-9]{2}) ?");
|
|
regxProj.setPattern("(\\+[^ ]+)");
|
|
regxCtx.setPattern("(@[^ ]+)");
|
|
|
|
qDebug() << "I am life! " << this;
|
|
|
|
}
|
|
|
|
TaskModel::~TaskModel() {
|
|
delete toast;
|
|
qDebug() << "I am death! " << this;
|
|
}
|
|
|
|
int TaskModel::sort() const {
|
|
return mSort;
|
|
}
|
|
|
|
void TaskModel::setSort(const int &sort) {
|
|
QSettings settings;
|
|
mSort = sort;
|
|
settings.setValue("sort", sort);
|
|
setView();
|
|
emit listUpdated();
|
|
}
|
|
|
|
QVariant TaskModel::setting(const QString &setting, const QVariant &defVal) const {
|
|
QSettings settings;
|
|
if (settings.value(setting).isNull()) {
|
|
return defVal;
|
|
}
|
|
return settings.value(setting);
|
|
}
|
|
|
|
QString TaskModel::activeFilters() {
|
|
QString filterlist;
|
|
foreach(QString item, mFilters) {
|
|
filterlist = filterlist + item + ",";
|
|
}
|
|
if (filterlist.size() > 0) {
|
|
return " Filter: " + filterlist;
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
QString TaskModel::formatDate(const QDateTime &date) const {
|
|
return date.toString("yyyy-MM-dd");
|
|
}
|
|
|
|
void TaskModel::modifySetting(const QString &setting, const QVariant &value) {
|
|
QSettings settings;
|
|
settings.setValue(setting, value);
|
|
if (setting == "sort") mSort = value.toInt();
|
|
if (setting == "path") {
|
|
localfile.setPath(value.toString());
|
|
load();
|
|
}
|
|
}
|
|
|
|
void TaskModel::onFileDataChanged(QStringList data) {
|
|
localfile.save(data);
|
|
load();
|
|
setView();
|
|
}
|
|
|
|
void TaskModel::onArchiveData(QStringList data) {
|
|
localfile.saveArchive(data);
|
|
mArchiveData = data;
|
|
}
|
|
|
|
QVariantList TaskModel::filters() const {
|
|
QVariantList filters;
|
|
foreach (QString project, mProjects) {
|
|
QVariantMap map;
|
|
map["type"] = "Projects";
|
|
map["title"] = project;
|
|
if (mFilters.indexOf(project) >= 0) {
|
|
map["checked"] = true;
|
|
} else {
|
|
map["checked"] = false;
|
|
}
|
|
filters.append(map);
|
|
}
|
|
foreach (QString context, mContexts) {
|
|
QVariantMap map;
|
|
map["type"] = "Contexts";
|
|
map["title"] = context;
|
|
if (mFilters.indexOf(context) >= 0) {
|
|
map["checked"] = true;
|
|
} else {
|
|
map["checked"] = false;
|
|
}
|
|
filters.append(map);
|
|
}
|
|
foreach (QString duedate, mDueDates) {
|
|
QVariantMap map;
|
|
qDebug() << "::" + duedate;
|
|
map["type"] = "Due Dates";
|
|
map["title"] = duedate;
|
|
if (mFilters.indexOf(duedate) >= 0) {
|
|
map["checked"] = true;
|
|
} else {
|
|
map["checked"] = false;
|
|
}
|
|
filters.append(map);
|
|
}
|
|
return filters;
|
|
}
|
|
|
|
QVariantList TaskModel::tododata() {
|
|
QVariantList rData;
|
|
foreach (QString item, mTodoList) {
|
|
QVariantMap map = parseTask(item);
|
|
rData.append(map);
|
|
}
|
|
return rData;
|
|
}
|
|
|
|
void TaskModel::refresh() {
|
|
QSettings settings;
|
|
load();
|
|
emit listUpdated();
|
|
}
|
|
|
|
void TaskModel::commit() {
|
|
QSettings settings;
|
|
QString lnend = settings.value("windowsbreak").toBool() ? "\r\n" : "\n";
|
|
save();
|
|
}
|
|
|
|
void TaskModel::load() {
|
|
mTodoData.clear();
|
|
QStringList dummy;
|
|
dummy = localfile.load();
|
|
foreach (QString item, dummy) {
|
|
QVariantMap map = parseTask(item);
|
|
mTodoData.append(map);
|
|
}
|
|
|
|
mArchiveData = localfile.loadArchive();
|
|
}
|
|
|
|
void TaskModel::save() {
|
|
QStringList data;
|
|
foreach (QVariant map, mTodoData) {
|
|
QString listitem = buildTask(map.toMap());
|
|
data.append(listitem);
|
|
}
|
|
localfile.save(data);
|
|
load();
|
|
setView();
|
|
emit listUpdated();
|
|
}
|
|
|
|
void TaskModel::archive() {
|
|
QSettings settings;
|
|
QString lnend = settings.value("windowsbreak").toBool() ? "\r\n" : "\n";
|
|
foreach (QVariant map, mTodoData) {
|
|
QVariantMap task = map.toMap();
|
|
if (task["complete"].toBool()) {
|
|
QString architem = buildTask(task);
|
|
mArchiveData.append(architem);
|
|
mTodoData.removeAt(mTodoData.indexOf(task));
|
|
}
|
|
}
|
|
if (mArchiveData.size() > 0) {
|
|
localfile.saveArchive(mArchiveData);
|
|
commit();
|
|
} else {
|
|
load();
|
|
setView();
|
|
}
|
|
}
|
|
|
|
void TaskModel::search(const QString &text) {
|
|
mSearch = text;
|
|
setView();
|
|
}
|
|
|
|
void TaskModel::setValue(int idx, const QString &key, const QVariant &value) {
|
|
//qDebug() << "index: " << idx;
|
|
//qDebug() << "key: " << key;
|
|
//qDebug() << "value: " << value;
|
|
//qDebug() << "check: " << QVariantListDataModel::value(idx).value<QVariantMap>();
|
|
QSettings settings;
|
|
if (idx >=0 && idx < size()) {
|
|
QVariantMap current = QVariantListDataModel::value(idx).value<QVariantMap>();
|
|
QVariantMap updated = current;
|
|
updated[key] = value;
|
|
if (key == "complete" && value == true) {
|
|
updated["priority"] = "";
|
|
updated["dateCompleted"] = QDate::currentDate().toString("yyyy-MM-dd");
|
|
} else if (key == "complete" && value == false) {
|
|
updated["dateCompleted"] = "";
|
|
}
|
|
QString rtask = buildTask(updated);
|
|
updated["text"] = QVariant(rtask);
|
|
//qDebug() << "current: " << current;
|
|
//qDebug() << "idx: " << idx;
|
|
//qDebug() << "orig idx: " << mTodoData.indexOf(current);
|
|
//qDebug() << "test0:" << mTodoData.value(0);
|
|
//qDebug() << "testX:" << mTodoData.value(mTodoData.size()-1);
|
|
int origidx = mTodoData.indexOf(current);
|
|
mTodoData.replace(origidx, updated);
|
|
//setView();
|
|
commit();
|
|
if (key == "complete" && value == true && settings.value("autoarchive").toBool()) {
|
|
archive();
|
|
}
|
|
}
|
|
//qDebug() << "new: " << QVariantListDataModel::value(idx).value<QVariantMap>();
|
|
}
|
|
|
|
void TaskModel::exportFiles(const QStringList &files) {
|
|
|
|
QString epath = files[0];
|
|
QRegExp epathrgx("todo.txt$");
|
|
epath.replace(epathrgx, "");
|
|
|
|
LocalFile exportfrom;
|
|
LocalFile exportto;
|
|
exportto.setPath(epath);
|
|
|
|
QStringList todo = exportfrom.load();
|
|
QStringList done = exportfrom.loadArchive();
|
|
|
|
exportto.save(todo);
|
|
exportto.saveArchive(done);
|
|
}
|
|
|
|
void TaskModel::promptPurgeSandbox() {
|
|
dialog = new SystemDialog("PURGE", "CANCEL");
|
|
dialog->setTitle("Purge local sandbox");
|
|
dialog->setBody("Erase local sandbox files? WARNING This cannot be undone.");
|
|
|
|
bool success = QObject::connect(dialog, SIGNAL(finished(bb::system::SystemUiResult::Type)),
|
|
this, SLOT(onConfirmPurge(bb::system::SystemUiResult::Type)));
|
|
|
|
if (success) {
|
|
dialog->show();
|
|
} else {
|
|
dialog->deleteLater();
|
|
}
|
|
}
|
|
|
|
void TaskModel::onConfirmPurge(bb::system::SystemUiResult::Type value) {
|
|
QStringList empty;
|
|
LocalFile sandbox;
|
|
switch(value) {
|
|
case bb::system::SystemUiResult::ConfirmButtonSelection:
|
|
qDebug() << "PURGE";
|
|
sandbox.save(empty);
|
|
sandbox.saveArchive(empty);
|
|
load();
|
|
setView();
|
|
break;
|
|
case bb::system::SystemUiResult::CancelButtonSelection:
|
|
qDebug() << "CANCEL";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
dialog->deleteLater();
|
|
}
|
|
|
|
void TaskModel::promptDelete(int idx) {
|
|
dialog = new SystemDialog("DELETE", "CANCEL");
|
|
dialog->setTitle("Delete Task");
|
|
dialog->setBody("Permanently delete this Entry?");
|
|
|
|
bool success = QObject::connect(dialog, SIGNAL(finished(bb::system::SystemUiResult::Type)),
|
|
this, SLOT(onDeleteTask(bb::system::SystemUiResult::Type)));
|
|
|
|
if (success) {
|
|
mDelete = idx;
|
|
dialog->show();
|
|
} else {
|
|
mDelete = -1;
|
|
dialog->deleteLater();
|
|
}
|
|
|
|
}
|
|
|
|
void TaskModel::onDeleteTask(bb::system::SystemUiResult::Type value) {
|
|
switch(value) {
|
|
case bb::system::SystemUiResult::ConfirmButtonSelection:
|
|
//qDebug() << "DELETE";
|
|
delTask(mDelete);
|
|
break;
|
|
case bb::system::SystemUiResult::CancelButtonSelection:
|
|
//qDebug() << "CANCEL";
|
|
mDelete = -1;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
dialog->deleteLater();
|
|
}
|
|
|
|
void TaskModel::delTask(int idx) {
|
|
if (idx >=0 && idx < size()) {
|
|
QVariantMap current = QVariantListDataModel::value(idx).value<QVariantMap>();
|
|
int origidx = mTodoData.indexOf(current);
|
|
//qDebug() << "idx: " << idx;
|
|
//qDebug() << "orig idx: " << mTodoData.indexOf(current);
|
|
mTodoData.removeAt(origidx);
|
|
//setView();
|
|
commit();
|
|
mDelete = -1;
|
|
}
|
|
}
|
|
|
|
void TaskModel::changeFilter(const QString &filter, const bool &enabled) {
|
|
int idx = mFilters.indexOf(filter);
|
|
if (enabled) {
|
|
if (idx == -1) mFilters.append(filter);
|
|
} else {
|
|
if (idx >= 0) mFilters.removeAt(idx);
|
|
}
|
|
//qDebug() << "Filter List: " << mFilters;
|
|
}
|
|
|
|
void TaskModel::resetFilter() {
|
|
mFilters.clear();
|
|
setView();
|
|
toast->setBody("Filters removed.");
|
|
toast->show();
|
|
}
|
|
|
|
void TaskModel::addFullTask(const QString &task) {
|
|
QVariantMap map = parseTask(task);
|
|
QSettings settings;
|
|
if (settings.value("datenew").toBool()) {
|
|
map["dateCreated"] = QDate::currentDate().toString("yyyy-MM-dd");
|
|
}
|
|
mTodoData.append(map);
|
|
//setView();
|
|
commit();
|
|
}
|
|
|
|
void TaskModel::updateTask(int idx, const QString &task) {
|
|
if (idx >=0 && idx < size()) {
|
|
QVariantMap current = QVariantListDataModel::value(idx).value<QVariantMap>();
|
|
int origidx = mTodoData.indexOf(current);
|
|
QVariantMap map = parseTask(task);
|
|
if (origidx >= 0) {
|
|
mTodoData.replace(origidx, map);
|
|
//setView();
|
|
commit();
|
|
} else {
|
|
// TODO produce some error message
|
|
qDebug() << "ERROR: origidx = " << origidx;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVariantMap TaskModel::parseTask(QString rawTask) {
|
|
QVariantMap task;
|
|
int pos = 0;
|
|
|
|
QString item = rawTask;
|
|
bool complete = false;
|
|
QString priority = "";
|
|
QString dateCompleted = "";
|
|
QString dateCreated = "";
|
|
QString dateDue = "";
|
|
QString detail = "";
|
|
|
|
//qDebug() << "..parsing projects..";
|
|
QStringList prj;
|
|
pos = 0;
|
|
while ((pos = regxProj.indexIn(item,pos)) != -1) {
|
|
prj << regxProj.cap(1);
|
|
pos += regxProj.matchedLength();
|
|
}
|
|
if (prj.size() > 0) {
|
|
foreach (QString p, prj) {
|
|
if (mProjects.indexOf(p) < 0) {
|
|
mProjects << p;
|
|
}
|
|
}
|
|
}
|
|
//qDebug() << "my projects: " << mProjects;
|
|
|
|
//qDebug() << "..parsing contexts..";
|
|
QStringList ctx;
|
|
pos = 0;
|
|
while ((pos = regxCtx.indexIn(item,pos)) != -1) {
|
|
ctx << regxCtx.cap(1);
|
|
pos += regxCtx.matchedLength();
|
|
}
|
|
if (ctx.size() > 0) {
|
|
foreach (QString c, ctx) {
|
|
if (mContexts.indexOf(c) < 0) {
|
|
mContexts << c;
|
|
}
|
|
}
|
|
}
|
|
//qDebug() << "my contexts: " << mContexts;
|
|
|
|
QRegExp regxDue("due:([0-9]{4}-[0-9]{2}-[0-9]{2}) ?");
|
|
if (regxDue.indexIn(item,0) != -1) {
|
|
dateDue = regxDue.cap(1);
|
|
QString duestr = "due:" + dateDue;
|
|
if (dateDue.length() > 0) {
|
|
if (mDueDates.indexOf(duestr) < 0) {
|
|
mDueDates << duestr;
|
|
}
|
|
}
|
|
}
|
|
|
|
//qDebug() << "---*****---";
|
|
if (regxComp.indexIn(item) == 0) {
|
|
//qDebug() << "complete: true";
|
|
complete = true;
|
|
item.replace(regxComp, "");
|
|
pos = 0;
|
|
QStringList dates;
|
|
while ((pos = regxDate.indexIn(item,pos)) != -1) {
|
|
dates << regxDate.cap(1);
|
|
pos += regxDate.matchedLength();
|
|
}
|
|
if (dates.size() > 0) {
|
|
//qDebug() << "completed: " << dates.at(0);
|
|
dateCompleted = dates.at(0);
|
|
if (dates.size() >= 2) {
|
|
//qDebug() << "created: " << dates.at(1);
|
|
dateCreated = dates.at(1);
|
|
pos = regxDate.indexIn(item,0);
|
|
item.replace(QRegExp("^([0-9]{4}-[0-9]{2}-[0-9]{2} )"), "");
|
|
}
|
|
}
|
|
pos = regxDate.indexIn(item,0);
|
|
item.replace(QRegExp("^([0-9]{4}-[0-9]{2}-[0-9]{2} )"), "");
|
|
} else if (regxDate.indexIn(item) >= 0 && regxDate.indexIn(item) < 5) {
|
|
dateCreated = regxDate.cap(1);
|
|
// item.replace(regxDate.cap(1) + " ", "");
|
|
// item.replace(QRegExp("^([0-9]{4}-[0-9]{2}-[0-9]{2} )"), "");
|
|
item.remove(regxDate.indexIn(item), 11);
|
|
}
|
|
if (regxPri.indexIn(item) >= 0) {
|
|
//qDebug() << "priority: " << regxPri.cap(1);
|
|
priority = regxPri.cap(1);
|
|
item.replace(regxPri, "");
|
|
}
|
|
detail = item;
|
|
|
|
task["complete"] = QVariant(complete);
|
|
task["priority"] = QVariant(priority);
|
|
task["dateCompleted"] = QVariant(dateCompleted);
|
|
task["dateCreated"] = QVariant(dateCreated);
|
|
task["dateDue"] = QVariant(dateDue);
|
|
task["detail"] = QVariant(detail);
|
|
task["text"] = QVariant(rawTask);
|
|
|
|
return task;
|
|
}
|
|
|
|
QString TaskModel::buildTask(const QVariantMap &task) {
|
|
QString text = "";
|
|
bool complete = task["complete"].toBool();
|
|
QString priority = task["priority"].toString();
|
|
QString dateCompleted = task["dateCompleted"].toString();
|
|
QString dateCreated = task["dateCreated"].toString();
|
|
QString detail = task["detail"].toString();
|
|
if (complete) text = text + "x ";
|
|
if (priority.length() > 0) text = text + "(" + priority + ") ";
|
|
if (dateCompleted.length() > 0) text = text + dateCompleted + " ";
|
|
if (dateCreated.length() > 0) text = text + dateCreated + " ";
|
|
if (detail.length() > 0) text = text + detail;
|
|
return text;
|
|
}
|
|
|
|
bool TaskModel::sortByPriority(const QString &a, const QString &b) {
|
|
QString matchA;
|
|
QString matchB;
|
|
|
|
QRegExp pri("\\(([A-Z])\\) ");
|
|
QRegExp done("^x ");
|
|
QRegExp date("([0-9]{4}-[0-9]{2}-[0-9]{2}) ?");
|
|
QRegExp sca("^@");
|
|
QRegExp scp("^\\+");
|
|
|
|
if (pri.indexIn(a) >= 0) {
|
|
matchA = pri.cap(1);
|
|
} else if (done.indexIn(a) == 0) {
|
|
matchA = "}";
|
|
} else {
|
|
matchA = a.toLower();
|
|
matchA.replace(date,"");
|
|
matchA.replace(sca,"");
|
|
matchA.replace(scp,"");
|
|
}
|
|
//qDebug() << "a: " << a;
|
|
//qDebug() << "matchA: " << matchA;
|
|
if (pri.indexIn(b) >= 0) {
|
|
matchB = pri.cap(1);
|
|
} else if (done.indexIn(b) == 0) {
|
|
matchB = "}";
|
|
} else {
|
|
matchB = b.toLower();
|
|
matchB.replace(date,"");
|
|
matchB.replace(sca,"");
|
|
matchB.replace(scp,"");
|
|
}
|
|
//qDebug() << "b: " << b;
|
|
//qDebug() << "matchB: " << matchB;
|
|
return matchA < matchB;
|
|
}
|
|
|
|
bool TaskModel::sortByText(const QString &a, const QString &b) {
|
|
QString matchA = a;
|
|
QString matchB = b;
|
|
|
|
QRegExp pri("\\([A-Z]\\) ");
|
|
QRegExp done("^x ");
|
|
QRegExp date("([0-9]{4}-[0-9]{2}-[0-9]{2}) ?");
|
|
QRegExp sca("^@");
|
|
QRegExp scp("^\\+");
|
|
|
|
matchA.replace(done, "");
|
|
matchA.replace(pri, "");
|
|
matchA.replace(date, "");
|
|
matchA.replace(sca,"");
|
|
matchA.replace(scp,"");
|
|
//qDebug() << "A: " << matchA;
|
|
matchB.replace(done, "");
|
|
matchB.replace(pri, "");
|
|
matchB.replace(date, "");
|
|
matchB.replace(sca,"");
|
|
matchB.replace(scp,"");
|
|
//qDebug() << "B: " << matchB;
|
|
|
|
return matchA.toLower() < matchB.toLower();
|
|
}
|
|
|
|
bool TaskModel::sortByDueDate(const QString &a, const QString &b) {
|
|
QString matchA = a;
|
|
QString matchB = b;
|
|
|
|
QRegExp regxDue("(due:)([0-9]{4}-[0-9]{2}-[0-9]{2}) ?");
|
|
QRegExp pri("\\([A-Z]\\) ");
|
|
QRegExp done("^x ");
|
|
QRegExp date("([0-9]{4}-[0-9]{2}-[0-9]{2}) ?");
|
|
QRegExp sca("^@");
|
|
QRegExp scp("^\\+");
|
|
|
|
if (regxDue.indexIn(a,0) != -1) {
|
|
matchA = regxDue.cap(2);
|
|
} else if (done.indexIn(a) == 0) {
|
|
matchA = "}";
|
|
} else {
|
|
matchA.replace(done, "");
|
|
matchA.replace(pri, "");
|
|
matchA.replace(date, "");
|
|
matchA.replace(sca,"");
|
|
matchA.replace(scp,"");
|
|
}
|
|
|
|
if (regxDue.indexIn(b,0) != -1) {
|
|
matchB = regxDue.cap(2);
|
|
} else if (done.indexIn(b) == 0) {
|
|
matchB = "}";
|
|
} else {
|
|
matchB.replace(done, "");
|
|
matchB.replace(pri, "");
|
|
matchB.replace(date, "");
|
|
matchB.replace(sca,"");
|
|
matchB.replace(scp,"");
|
|
}
|
|
|
|
return matchA.toLower() < matchB.toLower();
|
|
}
|
|
|
|
void TaskModel::setView() {
|
|
|
|
mTodoList.clear();
|
|
mProjects.clear();
|
|
mContexts.clear();
|
|
mDueDates.clear();
|
|
//qDebug() << "::" << mFilters;
|
|
foreach (QVariant map, mTodoData) {
|
|
QString listitem = buildTask(map.toMap());
|
|
if (mSearch.length() > 0) {
|
|
if (listitem.contains(mSearch, Qt::CaseInsensitive)) {
|
|
mTodoList.append(listitem);
|
|
}
|
|
} else if (mFilters.length() > 0) {
|
|
// TODO this results in an "or" condition, probably should be "and" instead
|
|
foreach (QString filter, mFilters) {
|
|
if (listitem.indexOf(filter) >=0) {
|
|
mTodoList.append(listitem);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
mTodoList.append(listitem);
|
|
}
|
|
}
|
|
clear();
|
|
|
|
switch (mSort) {
|
|
case PRIORITY:
|
|
qSort(mTodoList.begin(), mTodoList.end(), sortByPriority);
|
|
break;
|
|
case ID:
|
|
// this is the default
|
|
break;
|
|
case TEXT:
|
|
qSort(mTodoList.begin(), mTodoList.end(), sortByText);
|
|
break;
|
|
case DUE:
|
|
qSort(mTodoList.begin(), mTodoList.end(), sortByDueDate);
|
|
break;
|
|
}
|
|
|
|
foreach (QString item, mTodoList) {
|
|
QVariantMap map = parseTask(item);
|
|
append(map);
|
|
}
|
|
emit listUpdated();
|
|
}
|