Compare commits

...

10 commits

Author SHA1 Message Date
b52a0b4b34 Sort modes 2025-01-08 21:25:40 +03:00
72b00399bf Better title for event editor 2025-01-08 17:19:50 +03:00
be6ad84398 Haptics everywhere 2025-01-08 17:18:38 +03:00
724384db88 Display queue name in prompt dialog 2025-01-08 15:35:05 +03:00
5bc78a1a30 Fix dark mode crash 2025-01-06 04:19:03 +03:00
8a9ad51d3b python utils 2024-07-15 17:18:17 +03:00
02da4a97b5 bump version 2024-06-24 01:27:37 +03:00
1802a0e022 word-based tag search with respect to order 2024-06-24 00:42:34 +03:00
862160d1ef debug suffix for debug build 2024-06-24 00:42:16 +03:00
fc2e255d31 Remove excessive permissions 2024-06-24 00:40:42 +03:00
31 changed files with 1580 additions and 243 deletions

3
.gitignore vendored
View file

@ -47,3 +47,6 @@ captures/
hs_err_pid*
*.idea*
# Build metadata
output-metadata.json

View file

@ -10,8 +10,8 @@ android {
applicationId "art.pegasko.yeeemp"
minSdk 21
targetSdk 34
versionCode 1003
versionName "1.003"
versionCode 1006
versionName "1.006"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -25,6 +25,10 @@ android {
shrinkResources = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix = '.debug'
debuggable = true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

View file

@ -3,8 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
@ -16,10 +15,6 @@
android:supportsRtl="true"
android:theme="@style/Theme.Yeeemp"
tools:targetApi="31">
<activity
android:name=".ui.activity.EventEditActivity"
android:theme="@style/Theme.Yeeemp.Red"
android:exported="false" />
<activity
android:name=".ui.activity.QueueListActivity"
android:exported="true"
@ -30,10 +25,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.activity.EventEditActivity"
android:theme="@style/Theme.Yeeemp.Red"
android:exported="false" />
<activity
android:name=".ui.activity.EventListActivity"
android:exported="false"
android:theme="@style/Theme.Yeeemp.Blue"></activity>
android:theme="@style/Theme.Yeeemp.Blue" />
</application>
</manifest>

View file

@ -0,0 +1,67 @@
/**
* Copyright 2024 pegasko
*
* 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.
*/
package art.pegasko.yeeemp.base;
import androidx.annotation.Nullable;
public class EventOrder {
public static String orderToString(@Nullable Order order) {
if (order == null) {
return null;
}
switch (order) {
case ID_ASC:
return "id_asc";
case ID_DESC:
return "id_desc";
case TIMESTAMP_ASC:
return "timestamp_asc";
case TIMESTAMP_DESC:
return "timestamp_desc";
default:
throw new RuntimeException("Not implemented for " + order);
}
}
public static Order orderFromString(@Nullable String order) {
if (order == null) {
return null;
}
switch (order) {
case "id_asc":
return Order.ID_ASC;
case "id_desc":
return Order.ID_DESC;
case "timestamp_asc":
return Order.TIMESTAMP_ASC;
case "timestamp_desc":
return Order.TIMESTAMP_DESC;
default:
return Order.ID_ASC;
// throw new RuntimeException("Not implemented for " + order);
}
}
public enum Order {
ID_ASC,
ID_DESC,
TIMESTAMP_ASC,
TIMESTAMP_DESC,
}
}

View file

@ -23,7 +23,7 @@ public interface Queue {
void setName(String name);
Event[] getEvents();
Event[] getEvents(EventOrder.Order order);
int getEventCount();

View file

@ -23,5 +23,5 @@ public interface QueueMaker {
void delete(Queue queue);
Queue[] list();
Queue[] list(QueueOrder.Order order);
}

View file

@ -0,0 +1,56 @@
/**
* Copyright 2024 pegasko
*
* 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.
*/
package art.pegasko.yeeemp.base;
import androidx.annotation.Nullable;
public class QueueOrder {
public static String orderToString(@Nullable Order order) {
if (order == null) {
return null;
}
switch (order) {
case ID:
return "id";
case NAME:
return "name";
default:
throw new RuntimeException("Not implemented for " + order);
}
}
public static Order orderFromString(@Nullable String order) {
if (order == null) {
return null;
}
switch (order) {
case "id":
return Order.ID;
case "name":
return Order.NAME;
default:
throw new RuntimeException("Not implemented for " + order);
}
}
public enum Order {
ID,
NAME,
}
}

View file

@ -57,11 +57,7 @@ public class EventImpl implements Event {
null
);
if (Utils.findResult(cursor)) {
return cursor.getLong(0);
}
return 0;
return Utils.getLongAndClose(cursor, 0);
}
}
@ -70,7 +66,13 @@ public class EventImpl implements Event {
synchronized (this.db) {
ContentValues cv = new ContentValues();
cv.put("timestamp", timestamp);
db.update("event", cv, "id = ?", new String[] { Integer.toString(this.getId()) });
db.update(
"event",
cv,
"id = ?",
new String[] { Integer.toString(this.getId()) }
);
}
}
@ -87,11 +89,7 @@ public class EventImpl implements Event {
null
);
if (Utils.findResult(cursor)) {
return cursor.getString(0);
}
return null;
return Utils.getStringAndClose(cursor, null);
}
}
@ -100,7 +98,13 @@ public class EventImpl implements Event {
synchronized (this.db) {
ContentValues cv = new ContentValues();
cv.put("comment", comment);
db.update("event", cv, "id = ?", new String[] { Integer.toString(this.getId()) });
db.update(
"event",
cv,
"id = ?",
new String[] { Integer.toString(this.getId()) }
);
}
}
@ -109,11 +113,19 @@ public class EventImpl implements Event {
*/
protected boolean hasTag(Tag tag) {
synchronized (this.db) {
Cursor cursor = db.query("event_tag", new String[] { "1" }, "event_id = ? AND tag_id = ?", new String[] {
Integer.toString(this.getId()), Integer.toString(tag.getId())
}, null, null, null);
Cursor cursor = db.query(
"event_tag",
new String[] { "1" },
"event_id = ? AND tag_id = ?",
new String[] {
Integer.toString(this.getId()), Integer.toString(tag.getId())
},
null,
null,
null
);
return Utils.findResult(cursor);
return Utils.findResultAndClose(cursor);
}
}
@ -195,6 +207,7 @@ public class EventImpl implements Event {
while (cursor.moveToNext()) {
tags[index++] = new TagImpl(this.db, cursor.getInt(0));
}
cursor.close();
return this.cachedTags = tags;
}

View file

@ -49,7 +49,9 @@ public class EventMakerImpl implements EventMaker {
null
);
if (Utils.findResult(cursor)) return new EventImpl(this.db, id);
if (Utils.findResultAndClose(cursor)) {
return new EventImpl(this.db, id);
}
} catch (SQLiteException e) {
Log.wtf(TAG, e);

View file

@ -25,10 +25,9 @@ import android.util.Log;
import androidx.annotation.NonNull;
import art.pegasko.yeeemp.base.Event;
import art.pegasko.yeeemp.base.EventOrder;
import art.pegasko.yeeemp.base.Queue;
import art.pegasko.yeeemp.base.Tag;
import art.pegasko.yeeemp.base.TagStat;
import kotlin.NotImplementedError;
public class QueueImpl implements Queue {
public static final String TAG = QueueImpl.class.getSimpleName();
@ -59,11 +58,7 @@ public class QueueImpl implements Queue {
null
);
if (Utils.findResult(cursor)) {
return cursor.getString(0);
}
return null;
return Utils.getStringAndClose(cursor, null);
}
}
@ -72,22 +67,63 @@ public class QueueImpl implements Queue {
synchronized (this.db) {
ContentValues cv = new ContentValues();
cv.put("name", name);
db.update("queue", cv, "id = ?", new String[] { Integer.toString(this.getId()) });
db.update(
"queue",
cv,
"id = ?",
new String[] { Integer.toString(this.getId()) }
);
}
}
@Override
public Event[] getEvents() {
public Event[] getEvents(EventOrder.Order order) {
synchronized (this.db) {
Cursor cursor = db.query(
"queue_event",
new String[] { "event_id" },
"queue_id = ?",
new String[] { Integer.toString(this.getId()) },
null,
null,
"event_id desc"
);
Cursor cursor;
// Requires JOIN
if ((order == EventOrder.Order.TIMESTAMP_ASC) || (order == EventOrder.Order.TIMESTAMP_DESC)) {
String query = (
"select\n" +
" queue_event.event_id\n" +
"from\n" +
" queue_event\n" +
"left join\n" +
" event\n" +
"on\n" +
" queue_event.event_id = event.id\n" +
"where\n" +
" queue_event.queue_id = ?\n" +
"order by\n" +
" timestamp "
);
if (order == EventOrder.Order.TIMESTAMP_ASC) {
query += "asc";
} else {
query += "desc";
}
cursor = db.rawQuery(
query,
new String[] { Integer.toString(this.getId()) }
);
} else {
cursor = db.query(
"queue_event",
new String[] { "event_id" },
"queue_id = ?",
new String[] { Integer.toString(this.getId()) },
null,
null,
( order == EventOrder.Order.ID_ASC ?
"event_id asc" :
( order == EventOrder.Order.ID_DESC ?
"event_id desc" :
null
))
);
}
if (cursor == null) {
return new Event[0];
@ -99,6 +135,7 @@ public class QueueImpl implements Queue {
while (cursor.moveToNext()) {
events[index++] = new EventImpl(this.db, cursor.getInt(0));
}
cursor.close();
return events;
}
@ -117,9 +154,7 @@ public class QueueImpl implements Queue {
null
);
if (!Utils.findResult(cursor)) return 0;
return cursor.getInt(0);
return Utils.getIntAndClose(cursor, 0);
}
}
@ -138,7 +173,7 @@ public class QueueImpl implements Queue {
null
);
return Utils.findResult(cursor);
return Utils.findResultAndClose(cursor);
}
}
@ -183,7 +218,38 @@ public class QueueImpl implements Queue {
public TagStat[] getGlobalTags() {
synchronized (this.db) {
Cursor cursor = db.rawQuery(
"select" + " tag_id,\n" + " count(*) as cnt\n" + "from (\n" + " select\n" + " event_id,\n" + " tag_id\n" + " from (\n" + " select\n" + " event_tag.event_id as event_id,\n" + " event_tag.tag_id as tag_id\n" + " from (\n" + " select\n" + " event_id\n" + " from\n" + " queue_event\n" + " where\n" + " queue_id = ?\n" + " ) as queue_event_temp\n" + " inner join\n" + " event_tag\n" + " on\n" + " (event_tag.event_id = queue_event_temp.event_id)\n" + " )\n" + " group by\n" + " event_id,\n" + " tag_id\n" + ")\n" + "group by\n" + " tag_id\n" + "order by\n" + " cnt desc",
"select" +
" tag_id,\n" +
" count(*) as cnt\n" +
"from (\n" +
" select\n" +
" event_id,\n" +
" tag_id\n" +
" from (\n" +
" select\n" +
" event_tag.event_id as event_id,\n" +
" event_tag.tag_id as tag_id\n" +
" from (\n" +
" select\n" +
" event_id\n" +
" from\n" +
" queue_event\n" +
" where\n" +
" queue_id = ?\n" +
" ) as queue_event_temp\n" +
" inner join\n" +
" event_tag\n" +
" on\n" +
" (event_tag.event_id = queue_event_temp.event_id)\n" +
" )\n" +
" group by\n" +
" event_id,\n" +
" tag_id\n" +
")\n" +
"group by\n" +
" tag_id\n" +
"order by\n" +
" cnt desc",
new String[] { Integer.toString(this.getId()) }
);

View file

@ -23,9 +23,10 @@ import android.database.sqlite.SQLiteException;
import android.util.Log;
import art.pegasko.yeeemp.base.Event;
import art.pegasko.yeeemp.base.EventMaker;
import art.pegasko.yeeemp.base.EventOrder;
import art.pegasko.yeeemp.base.Queue;
import art.pegasko.yeeemp.base.QueueMaker;
import art.pegasko.yeeemp.base.QueueOrder;
public class QueueMakerImpl implements QueueMaker {
public static final String TAG = EventMakerImpl.class.getSimpleName();
@ -40,16 +41,19 @@ public class QueueMakerImpl implements QueueMaker {
public Queue getById(int id) {
synchronized (this.db) {
try {
Cursor cursor = db.query("queue",
new String[] { "1" },
"id = ?",
new String[] { Integer.toString(id) },
null,
null,
null
Cursor cursor = db.query(
"queue",
new String[] { "1" },
"id = ?",
new String[] { Integer.toString(id) },
null,
null,
null
);
if (Utils.findResult(cursor)) return new QueueImpl(this.db, id);
if (Utils.findResultAndClose(cursor)) {
return new QueueImpl(this.db, id);
}
} catch (SQLiteException e) {
Log.wtf(TAG, e);
@ -76,9 +80,22 @@ public class QueueMakerImpl implements QueueMaker {
}
@Override
public Queue[] list() {
public Queue[] list(QueueOrder.Order order) {
synchronized (this.db) {
Cursor cursor = db.query("queue", new String[] { "id" }, null, null, null, null, null);
Cursor cursor = db.query(
"queue",
new String[] { "id" },
null,
null,
null,
null,
( order == QueueOrder.Order.ID ?
"rowid" :
( order == QueueOrder.Order.NAME ?
"name" :
null
))
);
if (cursor == null) {
return new Queue[0];
@ -90,6 +107,7 @@ public class QueueMakerImpl implements QueueMaker {
while (cursor.moveToNext()) {
queues[index++] = new QueueImpl(this.db, cursor.getInt(0));
}
cursor.close();
return queues;
}
@ -101,7 +119,8 @@ public class QueueMakerImpl implements QueueMaker {
// Drop events
try {
for (Event event : queue.getEvents()) {
// TODO: Foreign key for cascade delete
for (Event event : queue.getEvents(EventOrder.Order.ID_DESC)) {
db.delete("event", "id = ?", new String[] { Integer.toString(event.getId()) });
}
} catch (SQLiteException e) {

View file

@ -57,8 +57,9 @@ public class TagImpl implements Tag {
null
);
if (Utils.findResult(cursor)) {
return this._cached_name = cursor.getString(0);
String result = Utils.getStringAndClose(cursor, null);
if (result != null) {
return this._cached_name = result;
}
return null;

View file

@ -47,10 +47,12 @@ public class TagMakerImpl implements TagMaker {
null,
null
);
if (Utils.findResult(cursor)) {
int id = Utils.getIntAndClose(cursor, -1);
if (id != -1) {
return new TagImpl(
db,
cursor.getInt(0)
id
);
}
} catch (SQLiteException e) {

View file

@ -20,9 +20,85 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
public class Utils {
public static boolean findResult(Cursor cursor) {
if (cursor == null) return false;
cursor.moveToFirst();
return cursor.getCount() != 0;
public static boolean findResultAndClose(Cursor cursor) {
if (cursor == null)
return false;
boolean hasResult = cursor.moveToFirst();
if (!hasResult) {
cursor.close();
return false;
}
boolean result = cursor.getCount() != 0;
cursor.close();
return result;
}
public static int getIntAndClose(Cursor cursor, int def) {
if (cursor == null)
return def;
boolean hasResult = cursor.moveToFirst();
if (!hasResult) {
cursor.close();
return def;
}
if (cursor.getCount() == 0) {
cursor.close();
return def;
}
int result = cursor.getInt(0);
cursor.close();
return result;
}
public static long getLongAndClose(Cursor cursor, long def) {
if (cursor == null)
return def;
boolean hasResult = cursor.moveToFirst();
if (!hasResult) {
cursor.close();
return def;
}
if (cursor.getCount() == 0) {
cursor.close();
return def;
}
long result = cursor.getLong(0);
cursor.close();
return result;
}
public static String getStringAndClose(Cursor cursor, String def) {
if (cursor == null)
return def;
boolean hasResult = cursor.moveToFirst();
if (!hasResult) {
cursor.close();
return def;
}
if (cursor.getCount() == 0) {
cursor.close();
return def;
}
String result = cursor.getString(0);
cursor.close();
return result;
}
}

View file

@ -0,0 +1,5 @@
package art.pegasko.yeeemp.ui.activity;
public class Common {
public static final String PREFS_NAME = "preferences";
}

View file

@ -135,7 +135,7 @@ public class EventEditActivity extends AppCompatActivity {
binding = ActivityEventEditBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
getSupportActionBar().setTitle(getSupportActionBar().getTitle() + " / " + (event == null ? "Create" : "Edit") + " Event");
getSupportActionBar().setTitle(queue.getName() + " / " + (event == null ? "Create" : "Edit") + " Event");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
@ -209,6 +209,8 @@ public class EventEditActivity extends AppCompatActivity {
return true;
});
binding.fab.setOnClickListener(view -> {
Utils.hapticTick(view);
// Finalize values
this.eventContainer.comment = this.binding.eventEditContent.eventEditComment.getText().toString().trim();

View file

@ -27,34 +27,37 @@ import android.widget.TextView;
import java.util.ArrayList;
import art.pegasko.yeeemp.base.Tag;
import art.pegasko.yeeemp.base.TagStat;
public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
public static final String TAG = EventEditTagsAdapter.class.getSimpleName();
private ArrayList<TagStat> tags;
private ArrayList<TagStat> tags; // used by parent as container for filtered elements, changes as suer types
private ArrayList<TagStat> tagsAll; // used as container with backup of all unfiltered tags, does not change
private LayoutInflater inflater;
private final int viewResourceId;
private final int viewFieldId;
@SuppressWarnings("unchecked")
public EventEditTagsAdapter(Context context, int viewResourceId, int viewFieldId, ArrayList<TagStat> tags) {
super(context, viewResourceId, tags);
this.tags = tags;
this.tagsAll = (ArrayList<TagStat>) tags.clone();
this.viewResourceId = viewResourceId;
this.viewFieldId = viewFieldId;
this.inflater = LayoutInflater.from(context);
}
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) convertView = inflater.inflate(this.viewResourceId, parent, false);
public View getView(int position, View tagView, ViewGroup parent) {
if (tagView == null)
tagView = inflater.inflate(this.viewResourceId, parent, false);
TextView text;
try {
if (this.viewFieldId == 0) {
text = (TextView) convertView;
text = (TextView) tagView;
} else {
text = convertView.findViewById(this.viewFieldId);
text = tagView.findViewById(this.viewFieldId);
}
if (text == null) {
@ -68,7 +71,7 @@ public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
text.setText(tags.get(position).tag.getName() + " (" + tags.get(position).count + ")");
return convertView;
return tagView;
}
@Override
@ -80,47 +83,150 @@ public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
// TODO: Stop using mutable global
// Reusable filter result
private ArrayList<TagStat> tagsSuggestions = new ArrayList<TagStat>();
private String prevConstraint = null;
@Override
public String convertResultToString(Object resultValue) {
String str = ((TagStat) (resultValue)).tag.getName();
return str;
return ((TagStat) (resultValue)).tag.getName();
}
/**
* Check if constraint is reducing previous.
*
* Constraint (C) reduces previous (P) if `len(C) >= len(P)` and `C.startsWith(P)`.
*
* This is means that user has typed some text to the end of input field and matching items can be filtered
* from previous filter result.
* In the opposite case, the set of matching items grows and required full rebuild from scratch.
*/
private boolean checkIfPossibleReduceToConstraint(String prevConstraint, String constraint) {
if (prevConstraint == null)
return false;
return (constraint.length() >= prevConstraint.length()) && (constraint.startsWith(prevConstraint));
}
private String makeConstraintFromUserInput(CharSequence userInput) {
return userInput.toString().toLowerCase().trim();
}
private boolean hasChangedConstraint(String prevConstraint, String newConstraint) {
if (prevConstraint == null || newConstraint == null)
return true;
return !prevConstraint.equals(newConstraint);
}
private void reduceTagsSuggestions(TagMatcher matcher) {
for (int index = 0; index < tagsSuggestions.size(); ) {
if (!matcher.isMatch(tagsSuggestions.get(index).tag))
tagsSuggestions.remove(index);
else
index += 1;
}
}
private void filterTagsSuggestions(TagMatcher matcher) {
tagsSuggestions.clear();
for (TagStat tag : tagsAll) {
if (matcher.isMatch(tag.tag)) {
tagsSuggestions.add(tag);
}
}
}
private void prepareTagsSuggestions(CharSequence constraint) {
String newConstraint = makeConstraintFromUserInput(constraint);
TagMatcher matcher = new TagMatcher(newConstraint);
// TODO: Use better search strategy and optimize search in non-reducing mode
if (hasChangedConstraint(prevConstraint, newConstraint)) {
if (checkIfPossibleReduceToConstraint(prevConstraint, newConstraint))
reduceTagsSuggestions(matcher);
else
filterTagsSuggestions(matcher);
prevConstraint = newConstraint;
}
}
private void resetTagsSuggestions() {
prevConstraint = null;
tagsSuggestions.clear();
}
private FilterResults makeFilterResults() {
FilterResults filterResults = new FilterResults();
// TODO: Spank me for using global mutable instance for returning immutable result (causes concurrent modification when publishResults())
filterResults.values = tagsSuggestions;
filterResults.count = tagsSuggestions.size();
return filterResults;
}
private FilterResults makeEmptyFilterResults() {
return new FilterResults();
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
synchronized (tagsSuggestions) {
synchronized (this) {
if (constraint != null) {
tagsSuggestions.clear();
for (TagStat tag : tags) {
if (tag.tag.getName().contains(constraint.toString().toLowerCase())) {
tagsSuggestions.add(tag);
}
}
FilterResults filterResults = new FilterResults();
// TODO: Spank me for using global mutable instance for returning immutable result (causes concurrent modification when publishResults())
filterResults.values = tagsSuggestions;
filterResults.count = tagsSuggestions.size();
return filterResults;
prepareTagsSuggestions(constraint);
return makeFilterResults();
} else {
return new FilterResults();
resetTagsSuggestions();
return makeEmptyFilterResults();
}
}
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results.values == null)
return;
synchronized (this) {
if (results == null)
return;
synchronized (results.values) {
ArrayList<TagStat> filteredList = (ArrayList<TagStat>) results.values;
if (results.count > 0) {
if (results.values == null) {
clear();
for (TagStat c : filteredList) {
add(c);
}
notifyDataSetChanged();
return;
}
// TODO: Do not republish results if no data changed
ArrayList<TagStat> filteredList = (ArrayList<TagStat>) results.values;
clear();
for (TagStat c : filteredList) {
add(c);
}
notifyDataSetChanged();
}
}
/**
* Matcher to filter tags based on user-input constraint
*/
class TagMatcher {
private final String[] constraintParts;
public TagMatcher(String constraint) {
this.constraintParts = constraint.split(" +");
}
public boolean isMatch(Tag tag) {
String tagName = tag.getName();
int lastIndex = 0;
for (String seq : constraintParts) {
if (lastIndex >= tagName.length())
return false;
int newIndex = tagName.indexOf(seq, lastIndex);
if (newIndex == -1)
return false;
lastIndex = newIndex + seq.length();
}
return true;
}
}
};

View file

@ -16,25 +16,27 @@
package art.pegasko.yeeemp.ui.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import java.time.Duration;
import java.util.concurrent.ScheduledFuture;
import java.util.Objects;
import art.pegasko.yeeemp.R;
import art.pegasko.yeeemp.base.Event;
import art.pegasko.yeeemp.base.EventOrder;
import art.pegasko.yeeemp.base.Queue;
import art.pegasko.yeeemp.base.Wrapper;
import art.pegasko.yeeemp.databinding.ActivityEventListBinding;
@ -43,6 +45,9 @@ import art.pegasko.yeeemp.impl.Init;
public class EventListActivity extends AppCompatActivity {
public static final String TAG = EventListActivity.class.getSimpleName();
private static final String PREFS_UI_EVENT_ORDER = "ui-event-order";
private static final String PREFS_UI_EVENT_ORDER_DEFAULT = "id_desc";
private ActivityEventListBinding binding;
private RecyclerView eventList;
private EventRecyclerViewAdapter eventListAdapter;
@ -54,6 +59,25 @@ public class EventListActivity extends AppCompatActivity {
});
}
private CharSequence getEventOrderDisplayString(@Nullable EventOrder.Order order) {
if (order == null) {
throw new RuntimeException("Order is null");
}
switch (order) {
case ID_ASC:
return "created ascending";
case ID_DESC:
return "created descending";
case TIMESTAMP_ASC:
return "date ascending";
case TIMESTAMP_DESC:
return "date descending";
default:
throw new RuntimeException("Not implemented for " + order);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -82,6 +106,7 @@ public class EventListActivity extends AppCompatActivity {
return;
}
/* Bindings */
binding = ActivityEventListBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
@ -89,31 +114,73 @@ public class EventListActivity extends AppCompatActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
/* Menu handlers */
binding.toolbar.setOnMenuItemClickListener((MenuItem item) -> {
if (item.getItemId() == R.id.event_list_toolbar_menu_order) {
/* Get some options */
SharedPreferences prefs = this.getApplicationContext().getSharedPreferences(Common.PREFS_NAME, MODE_PRIVATE);
EventOrder.Order order = EventOrder.orderFromString(prefs.getString(PREFS_UI_EVENT_ORDER, PREFS_UI_EVENT_ORDER_DEFAULT));
/* Construct options */
EventOrder.Order[] values = EventOrder.Order.class.getEnumConstants();
if (values == null) {
// How did we get here?
throw new RuntimeException("EventOrder.Order.class.getEnumConstants() is Empty");
}
CharSequence[] choices = new CharSequence[values.length];
int currentSelection = -1;
for (int i = 0; i < values.length; ++i) {
choices[i] = this.getEventOrderDisplayString(values[i]);
if (values[i] == order) {
currentSelection = i;
}
}
/* Build dialog */
AlertDialog.Builder builder = new AlertDialog.Builder(EventListActivity.this);
builder.setTitle("Order by ..");
builder.setSingleChoiceItems(
choices,
currentSelection,
(dialog, which) -> {
EventOrder.Order newOrder = values[which];
// Save option
prefs.edit().putString(PREFS_UI_EVENT_ORDER, EventOrder.orderToString(newOrder)).apply();
// Apply
EventListActivity.this.invalidateOptionsMenu();
eventListAdapter.setOrder(newOrder);
runOnUiThread(() -> {
eventListAdapter.reloadItems();
});
// Close dialog
dialog.dismiss();
}
);
builder.show();
return true;
}
return false;
});
/* Get some options */
SharedPreferences prefs = this.getApplicationContext().getSharedPreferences(Common.PREFS_NAME, MODE_PRIVATE);
EventOrder.Order order = EventOrder.orderFromString(prefs.getString(PREFS_UI_EVENT_ORDER, PREFS_UI_EVENT_ORDER_DEFAULT));
/* Queue list */
eventListAdapter = new EventRecyclerViewAdapter(queue);
eventListAdapter.setOrder(order);
eventList = findViewById(R.id.content_event_list__list);
eventList.setLayoutManager(new LinearLayoutManager(this));
eventList.setAdapter(eventListAdapter);
// /* Swipe delete */
// ItemTouchHelper.SimpleCallback swipeCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
// @Override
// public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
// return false;
// }
//
// @Override
// public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// Snackbar.make(viewHolder.itemView, "undo delete event", 5000)
// .setAnchorView(R.id.fab)
// .setAction("Action", (View view) -> {
//
// }).show();
//
// ScheduledFuture<?> future = eventListAdapter.scheduleDeleteAt(viewHolder.getAdapterPosition());
// }
// };
/* FAB Listeners */
binding.fab.setOnLongClickListener((View view) -> {
Snackbar.make(view, "Create Event", Snackbar.LENGTH_LONG).setAnchorView(R.id.fab).setAction(
@ -124,6 +191,8 @@ public class EventListActivity extends AppCompatActivity {
return true;
});
binding.fab.setOnClickListener(view -> {
Utils.hapticTick(view);
Bundle extra = new Bundle();
extra.putInt("queue_id", this.queue.getId());
@ -137,6 +206,21 @@ public class EventListActivity extends AppCompatActivity {
updateList();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.event_list_toolbar_menu, menu);
/* Get some options */
SharedPreferences prefs = this.getApplicationContext().getSharedPreferences(Common.PREFS_NAME, MODE_PRIVATE);
EventOrder.Order order = EventOrder.orderFromString(prefs.getString(PREFS_UI_EVENT_ORDER, PREFS_UI_EVENT_ORDER_DEFAULT));
// Set item text based on sort mode
MenuItem item = menu.findItem(R.id.event_list_toolbar_menu_order);
item.setTitle(Objects.requireNonNull(item.getTitle()).toString() + ": " + getEventOrderDisplayString(order).toString());
return true;
}
@Override
protected void onResume() {
super.onResume();

View file

@ -18,11 +18,9 @@ package art.pegasko.yeeemp.ui.activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@ -34,12 +32,10 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import art.pegasko.yeeemp.R;
import art.pegasko.yeeemp.base.Event;
import art.pegasko.yeeemp.base.EventOrder;
import art.pegasko.yeeemp.base.Queue;
import art.pegasko.yeeemp.base.Tag;
import art.pegasko.yeeemp.base.Wrapper;
@ -51,6 +47,8 @@ class EventRecyclerViewAdapter extends RecyclerView.Adapter<EventRecyclerViewAda
private final Queue queue;
private Event[] events;
private EventOrder.Order order;
public EventRecyclerViewAdapter(Queue queue) {
super();
this.queue = queue;
@ -121,6 +119,8 @@ class EventRecyclerViewAdapter extends RecyclerView.Adapter<EventRecyclerViewAda
return true;
});
viewHolder.getBinding().eventListItemItem.setOnClickListener((View view) -> {
Utils.hapticTick(view);
Bundle extra = new Bundle();
extra.putInt("event_id", this.events[position].getId());
extra.putInt("queue_id", this.queue.getId());
@ -153,10 +153,14 @@ class EventRecyclerViewAdapter extends RecyclerView.Adapter<EventRecyclerViewAda
public void reloadItems() {
Handler handler = new Handler(Looper.getMainLooper());
Executors.newSingleThreadExecutor().execute(() -> {
this.events = this.queue.getEvents();
this.events = this.queue.getEvents(this.order);
handler.post(() -> {
this.notifyDataSetChanged();
});
});
}
public void setOrder(EventOrder.Order order) {
this.order = order;
}
}

View file

@ -16,44 +16,37 @@
package art.pegasko.yeeemp.ui.activity;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Environment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.PermissionChecker;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.io.File;
import java.security.Permission;
import java.util.Objects;
import art.pegasko.yeeemp.base.Queue;
import art.pegasko.yeeemp.base.QueueOrder;
import art.pegasko.yeeemp.base.Wrapper;
import art.pegasko.yeeemp.databinding.ActivityQueueListBinding;
import art.pegasko.yeeemp.R;
import art.pegasko.yeeemp.impl.DBWrapper;
import art.pegasko.yeeemp.impl.DataUtils;
import art.pegasko.yeeemp.impl.Init;
@ -63,6 +56,9 @@ public class QueueListActivity extends AppCompatActivity {
private static final int REQUEST_CODE_CREATE_FILE = 37;
private static final int REQUEST_CODE_OPEN_FILE = 19;
private static String PREFS_UI_QUEUE_ORDER = "ui-queue-order";
private static String PREFS_UI_QUEUE_ORDER_DEFAULT = "id";
private AppBarConfiguration appBarConfiguration;
private ActivityQueueListBinding binding;
private RecyclerView queueList;
@ -74,6 +70,21 @@ public class QueueListActivity extends AppCompatActivity {
});
}
private CharSequence getQueueOrderDisplayString(@Nullable QueueOrder.Order order) {
if (order == null) {
throw new RuntimeException("Order is null");
}
switch (order) {
case ID:
return "created";
case NAME:
return "name";
default:
throw new RuntimeException("Not implemented for " + order);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -86,9 +97,59 @@ public class QueueListActivity extends AppCompatActivity {
/* Toolbar menu */
binding.toolbar.inflateMenu(R.menu.queue_list_toolbar_menu);
// TODO: Better import / export / delete logic
binding.toolbar.setOnMenuItemClickListener((MenuItem item) -> {
if (item.getItemId() == R.id.queue_list_toolbar_menu_export) {
if (item.getItemId() == R.id.queue_list_toolbar_menu_order) {
/* Get some options */
SharedPreferences prefs = this.getApplicationContext().getSharedPreferences(Common.PREFS_NAME, MODE_PRIVATE);
QueueOrder.Order order = QueueOrder.orderFromString(prefs.getString(PREFS_UI_QUEUE_ORDER, PREFS_UI_QUEUE_ORDER_DEFAULT));
/* Construct options */
QueueOrder.Order[] values = QueueOrder.Order.class.getEnumConstants();
if (values == null) {
// How did we get here?
throw new RuntimeException("QueueOrder.Order.class.getEnumConstants() is Empty");
}
CharSequence[] choices = new CharSequence[values.length];
int currentSelection = -1;
for (int i = 0; i < values.length; ++i) {
choices[i] = this.getQueueOrderDisplayString(values[i]);
if (values[i] == order) {
currentSelection = i;
}
}
/* Build dialog */
AlertDialog.Builder builder = new AlertDialog.Builder(QueueListActivity.this);
builder.setTitle("Order by ..");
builder.setSingleChoiceItems(
choices,
currentSelection,
(dialog, which) -> {
QueueOrder.Order newOrder = values[which];
// Save option
prefs.edit().putString(PREFS_UI_QUEUE_ORDER, QueueOrder.orderToString(newOrder)).apply();
// Apply
QueueListActivity.this.invalidateOptionsMenu();
queueListAdapter.setOrder(newOrder);
runOnUiThread(() -> {
queueListAdapter.reloadItems();
});
// Close
dialog.dismiss();
}
);
builder.show();
return true;
} if (item.getItemId() == R.id.queue_list_toolbar_menu_export) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TITLE, "export_" + DataUtils.formatTs(System.currentTimeMillis()) + ".db");
@ -140,8 +201,13 @@ public class QueueListActivity extends AppCompatActivity {
return false;
});
/* Get some options */
SharedPreferences prefs = this.getApplicationContext().getSharedPreferences(Common.PREFS_NAME, MODE_PRIVATE);
QueueOrder.Order order = QueueOrder.orderFromString(prefs.getString(PREFS_UI_QUEUE_ORDER, PREFS_UI_QUEUE_ORDER_DEFAULT));
/* Queue list */
queueListAdapter = new QueueRecyclerViewAdapter();
queueListAdapter.setOrder(order);
queueList = findViewById(R.id.content_queue_list__list);
queueList.setLayoutManager(new LinearLayoutManager(this));
queueList.setAdapter(queueListAdapter);
@ -156,6 +222,8 @@ public class QueueListActivity extends AppCompatActivity {
return true;
});
binding.fab.setOnClickListener(view -> {
Utils.hapticTick(view);
Queue q = Wrapper.getQueueMaker().create();
q.setName("New Queue");
@ -273,6 +341,16 @@ public class QueueListActivity extends AppCompatActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.queue_list_toolbar_menu, menu);
/* Get some options */
SharedPreferences prefs = this.getApplicationContext().getSharedPreferences(Common.PREFS_NAME, MODE_PRIVATE);
QueueOrder.Order order = QueueOrder.orderFromString(prefs.getString(PREFS_UI_QUEUE_ORDER, PREFS_UI_QUEUE_ORDER_DEFAULT));
// Set item text based on sort mode
MenuItem item = menu.findItem(R.id.queue_list_toolbar_menu_order);
item.setTitle(Objects.requireNonNull(item.getTitle()).toString() + ": " + getQueueOrderDisplayString(order).toString());
return true;
}

View file

@ -17,11 +17,9 @@
package art.pegasko.yeeemp.ui.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@ -33,9 +31,8 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import art.pegasko.yeeemp.R;
import art.pegasko.yeeemp.base.Event;
import art.pegasko.yeeemp.base.Queue;
import art.pegasko.yeeemp.base.Tag;
import art.pegasko.yeeemp.base.QueueOrder;
import art.pegasko.yeeemp.base.Wrapper;
import art.pegasko.yeeemp.databinding.QueueListItemBinding;
@ -44,6 +41,8 @@ class QueueRecyclerViewAdapter extends RecyclerView.Adapter<QueueRecyclerViewAda
private Queue[] queues;
private QueueOrder.Order order;
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
@ -64,7 +63,7 @@ class QueueRecyclerViewAdapter extends RecyclerView.Adapter<QueueRecyclerViewAda
popupMenu.setOnMenuItemClickListener((MenuItem menuItem) -> {
if (menuItem.getItemId() == R.id.queue_list_item_action_menu_delete) {
new AlertDialog.Builder(view.getContext()).setTitle("Delete queue").setMessage(
"Are you sure you want to delete this queue?").setPositiveButton(
"Are you sure you want to delete " + queues[position].getName() + "?").setPositiveButton(
android.R.string.yes,
(dialog, which) -> {
Wrapper.getQueueMaker().delete(
@ -75,7 +74,7 @@ class QueueRecyclerViewAdapter extends RecyclerView.Adapter<QueueRecyclerViewAda
).setNegativeButton(android.R.string.no, null).setIcon(android.R.drawable.ic_dialog_alert).show();
} else if (menuItem.getItemId() == R.id.queue_list_item_action_menu_rename) {
AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
builder.setTitle("Title");
builder.setTitle("New name");
final EditText input = new EditText(view.getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
@ -100,6 +99,8 @@ class QueueRecyclerViewAdapter extends RecyclerView.Adapter<QueueRecyclerViewAda
return true;
});
viewHolder.getBinding().queueListItemItem.setOnClickListener((View view) -> {
Utils.hapticTick(view);
Bundle extra = new Bundle();
extra.putInt("queue_id", queues[position].getId());
@ -112,6 +113,8 @@ class QueueRecyclerViewAdapter extends RecyclerView.Adapter<QueueRecyclerViewAda
viewHolder.getBinding().queueListItemStats.setText(Integer.toString(queues[position].getEventCount()));
viewHolder.getBinding().queueListItemPlus.setOnClickListener((View view) -> {
Utils.hapticTick(view);
Bundle extra = new Bundle();
extra.putInt("queue_id", this.queues[position].getId());
@ -141,7 +144,11 @@ class QueueRecyclerViewAdapter extends RecyclerView.Adapter<QueueRecyclerViewAda
}
public void reloadItems() {
this.queues = Wrapper.getQueueMaker().list();
this.queues = Wrapper.getQueueMaker().list(this.order);
this.notifyDataSetChanged();
}
public void setOrder(QueueOrder.Order order) {
this.order = order;
}
}

View file

@ -16,6 +16,13 @@
package art.pegasko.yeeemp.ui.activity;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.HapticFeedbackConstants;
import android.view.View;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@ -55,4 +62,29 @@ public class Utils {
return 0;
return Math.abs(o.hashCode()) & ((1 << 16) - 1);
}
public static void hapticTick(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Vibration muted, do nothing
if (!view.isHapticFeedbackEnabled()) {
return;
}
// Success only if haptics available and worked
if (view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)) {
return;
}
}
// Fallback to basic vibration
Vibrator vibrator = (Vibrator) view.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
// TODO: Respect silent mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(10, 50));
} else {
vibrator.vibrate(10);
}
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/event_list_toolbar_menu_order"
android:title="Order by" />
</menu>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/queue_list_toolbar_menu_order"
android:title="Order by" />
<item
android:id="@+id/queue_list_toolbar_menu_export"
android:title="Export" />

View file

@ -1,9 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Yeeemp.Green" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
</style>
<!-- Base application theme. -->
<style name="Base.Theme.Yeeemp.Green" parent="Theme.Material3.DayNight.NoActionBar">
</style>
</resources>

View file

@ -1,17 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Yeeemp.Green" parent="Base.Theme.Yeeemp.Green">
<style name="Theme.Yeeemp.Green" parent="Theme.Yeeemp">
<item name="android:navigationBarColor">@color/pegasko_dark</item>
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="android:windowLightStatusBar">false</item>
</style>
<style name="Theme.Yeeemp.Blue" parent="Base.Theme.Yeeemp.Blue">
<style name="Theme.Yeeemp.Blue" parent="Theme.Yeeemp">
<item name="android:navigationBarColor">@color/pegasko_dark</item>
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="android:windowLightStatusBar">false</item>
</style>
<style name="Theme.Yeeemp.Red" parent="Base.Theme.Yeeemp.Red">
<style name="Theme.Yeeemp.Red" parent="Theme.Yeeemp">
<item name="android:navigationBarColor">@color/pegasko_dark</item>
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="android:windowLightStatusBar">false</item>

View file

@ -1,46 +1,3 @@
<resources>
<string name="app_name">Yeeemp</string>
<!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="lorem_ipsum">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
bibendum, vel congue leo egestas.\n\n
Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
est.\n\n
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
convallis.\n\n
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
</string>
<string name="title_activity_main">MainActivity</string>
</resources>

View file

@ -1,24 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Yeeemp" parent="ThemeOverlay.MaterialComponents.Dark">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/pegasko_white</item>
<item name="colorPrimaryVariant">@color/pegasko_light</item>
<item name="colorOnPrimary">@color/pegasko_white</item>
<!-- Secondary brand color. -->
<item name="colorPrimary">@color/pegasko_white</item>
<item name="colorPrimaryVariant">@color/pegasko_light</item>
<item name="colorOnPrimary">@color/pegasko_white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="statusBarBackground">@color/pegasko_dark</item>
<item name="statusBarForeground">@color/pegasko_dark</item>
</style>
<!-- Base application theme. -->
<style name="Base.Theme.Yeeemp" parent="Theme.Material3.Dark.NoActionBar">
<style name="Theme.Yeeemp" parent="Theme.Material3.Dark.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/pegasko_white</item>
<item name="colorPrimaryVariant">@color/pegasko_light</item>
@ -36,7 +18,7 @@
</style>
<!-- Green application theme. -->
<style name="Base.Theme.Yeeemp.Green" parent="Base.Theme.Yeeemp">
<style name="Theme.Yeeemp.Green" parent="Theme.Yeeemp">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/pegasko_green</item>
<item name="colorPrimaryVariant">@color/pegasko_dark_green</item>
@ -46,15 +28,10 @@
<item name="colorSecondary">@color/pegasko_green</item>
<item name="colorSecondaryVariant">@color/pegasko_dark_green</item>
<item name="colorOnSecondary">@color/pegasko_dark_green</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="statusBarBackground">@color/pegasko_dark</item>
<item name="statusBarForeground">@color/pegasko_dark</item>
</style>
<!-- Blue application theme. -->
<style name="Base.Theme.Yeeemp.Blue" parent="Base.Theme.Yeeemp">
<style name="Theme.Yeeemp.Blue" parent="Theme.Yeeemp">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/pegasko_blue</item>
<item name="colorPrimaryVariant">@color/pegasko_dark_blue</item>
@ -64,15 +41,10 @@
<item name="colorSecondary">@color/pegasko_blue</item>
<item name="colorSecondaryVariant">@color/pegasko_dark_blue</item>
<item name="colorOnSecondary">@color/pegasko_dark_blue</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="statusBarBackground">@color/pegasko_dark</item>
<item name="statusBarForeground">@color/pegasko_dark</item>
</style>
<!-- Red application theme. -->
<style name="Base.Theme.Yeeemp.Red" parent="Base.Theme.Yeeemp">
<style name="Theme.Yeeemp.Red" parent="Theme.Yeeemp">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/pegasko_red</item>
<item name="colorPrimaryVariant">@color/pegasko_dark_red</item>
@ -82,11 +54,6 @@
<item name="colorSecondary">@color/pegasko_red</item>
<item name="colorSecondaryVariant">@color/pegasko_dark_red</item>
<item name="colorOnSecondary">@color/pegasko_dark_red</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@color/pegasko_dark</item>
<item name="statusBarBackground">@color/pegasko_dark</item>
<item name="statusBarForeground">@color/pegasko_dark</item>
</style>
</resources>

7
python/README.md Normal file
View file

@ -0,0 +1,7 @@
# Python utilities
this directory contans set of python modules / files useful for working with database files (inclusing creation, modification, e.t.c).
# yeeemp
primary file is `yeeemp.py` useful for ORM-based exploration of database files. includes tests.

287
python/test_yeeemp.py Normal file
View file

@ -0,0 +1,287 @@
import sqlite3
import typing
import yeeemp
def create_db():
conn = sqlite3.connect(':memory:')
# yeeemp.DBUtil.drop('test.db')
# conn = sqlite3.connect('test.db')
yeeemp.DBUtil.init(conn)
return conn
def test_queue_create():
with create_db() as conn:
root = yeeemp.Root(conn)
queue_list = root.list_queue()
assert len(queue_list) == 0, 'expected empty queue list'
queue = root.create_queue()
assert queue.get_id() == 1, ''
assert queue.get_name() is None, 'no name set by default'
queue.set_name('aboba')
assert queue.get_name() == 'aboba', ''
queue_list = root.list_queue()
assert len(queue_list) == 1, 'expected not empty queue list'
def test_queue_tag_create():
with create_db() as conn:
root = yeeemp.Root(conn)
queue = root.create_queue()
queue.set_name('aboba')
queue_tags = root.list_queue_tag(queue)
assert len(queue_tags) == 0, 'expected empty tags list for new queue'
tag = root.create_tag(queue, 'gato')
assert tag is not None, ''
tag_lookup = root.get_tag_by_name(queue, 'gato')
assert tag_lookup is not None, 'expected find created tag'
assert tag.get_id() == tag_lookup.get_id() is not None, 'expected tag id match'
tag = root.create_tag(queue, 'pato')
assert tag is not None, ''
tag = root.create_tag(queue, 'gato')
assert tag is None, 'expected fail on duplicating tags'
queue_tags = root.list_queue_tag(queue)
assert len(queue_tags) == 2, 'expected not empty tags list for new queue'
queue = root.create_queue()
queue.set_name('bebra')
queue_tags = root.list_queue_tag(queue)
assert len(queue_tags) == 0, 'expected empty tags list for new queue'
def create_tags(root: yeeemp.Root, queue: yeeemp.Queue, prefix: str, n: int) -> list[yeeemp.Tag]:
tags = []
for idx in range(n):
tags.append(root.create_tag(queue, f'{ prefix }_{ idx }'))
return tags
def force_create_tags(root: yeeemp.Root, queue: yeeemp.Queue, prefix: str, n: int) -> list[yeeemp.Tag]:
tags = []
for idx in range(n):
tags.append(root.force_create_tag(queue, f'{ prefix }_{ idx }'))
return tags
def subproc_event_add_tags(
root: yeeemp.Root,
queue: yeeemp.Queue,
event: yeeemp.Event,
tags: list[yeeemp.Tag],
):
for tag in tags:
root.join_event_tag(event, tag)
def subproc_event_drop_tags(
root: yeeemp.Root,
queue: yeeemp.Queue,
event: yeeemp.Event,
):
root.delete_event_tag_all(event)
def subproc_event_check_no_tags(
root: yeeemp.Root,
queue: yeeemp.Queue,
event: yeeemp.Event,
):
event_tags = root.list_event_tag(event)
assert len(event_tags) == 0, 'expected no tags'
def subproc_event_check_has_tags(
root: yeeemp.Root,
queue: yeeemp.Queue,
event: yeeemp.Event,
tags: list[yeeemp.Tag],
):
"Check has all specified tags"
event_tags = root.list_event_tag(event)
event_tag_set = set([ tag.get_name() for tag in event_tags ])
assert all([ tag.get_name() in event_tag_set for tag in tags ]), 'event missing expected tags'
def subproc_event_check_has_only_tags(
root: yeeemp.Root,
queue: yeeemp.Queue,
event: yeeemp.Event,
tags: list[yeeemp.Tag],
):
"Check has only specified tags"
event_tags = root.list_event_tag(event)
event_tag_set = set([ tag.get_name() for tag in event_tags ])
expected_tag_set = set([ tag.get_name() for tag in tags ])
assert all([ tag.get_name() in expected_tag_set for tag in event_tags ]), 'event has unexpected tags tags'
assert all([ tag.get_name() in event_tag_set for tag in tags ]), 'event missing expected tags tags'
def subproc_map_tags_sample(
master_prefix: str,
mapper: typing.Callable[[str, int], None]
):
# append event tags
prefixes = [
master_prefix + 'bread',
master_prefix + 'gato',
master_prefix + 'potato',
master_prefix + 'aboba',
master_prefix + 'stonks'
]
populations = [
1,
102,
9127,
0,
2,
]
for (prefix, population) in zip(prefixes, populations):
mapper(prefix, population)
def test_event_create():
with create_db() as conn:
root = yeeemp.Root(conn)
queue = root.create_queue()
queue.set_name('aboba')
queue_events = root.list_queue_event(queue)
assert len(queue_events) == 0, 'expected empty event list in new queue'
event = root.create_event()
queue_events = root.list_queue_event(queue)
assert len(queue_events) == 0, 'expected empty event list because event is not attached'
event.set_comment('uwu')
assert event.get_comment() == 'uwu'
event.set_comment('owo')
assert event.get_comment() == 'owo'
event.set_timestamp(1029384756)
assert event.get_timestamp() == 1029384756
event.set_timestamp(732)
assert event.get_timestamp() == 732
# append event to queue
root.join_queue_event(queue, event)
queue_events = root.list_queue_event(queue)
assert len(queue_events) == 1, 'expected not empty event list in queue'
# Add & delete tags repeatedly
def clean_add_tags(prefix, population):
tags = force_create_tags(
root=root,
queue=queue,
prefix=prefix,
n=population,
)
subproc_event_add_tags(
root,
queue,
event,
tags,
)
subproc_event_check_has_only_tags(
root,
queue,
event,
tags
)
subproc_event_drop_tags(root, queue, event)
subproc_map_tags_sample(
'owo_',
clean_add_tags
)
# Add new tags & expect persisting
prev_tags = None
def dirty_add_tags(prefix, population):
nonlocal prev_tags
tags = force_create_tags(
root=root,
queue=queue,
prefix=prefix,
n=population,
)
subproc_event_add_tags(
root,
queue,
event,
tags,
)
if prev_tags is not None:
subproc_event_check_has_tags(
root,
queue,
event,
prev_tags
)
prev_tags = tags
subproc_map_tags_sample(
'uwu_',
dirty_add_tags
)
# New event with other tags must not mess with other events
event = root.create_event()
root.join_queue_event(queue, event)
tags = force_create_tags(
root=root,
queue=queue,
prefix='rawr',
n=17,
)
subproc_event_add_tags(
root,
queue,
event,
tags,
)
subproc_event_check_has_only_tags(
root,
queue,
event,
tags
)

492
python/yeeemp.py Normal file
View file

@ -0,0 +1,492 @@
import traceback
import sqlite3
import os
# .=" "=._.---.
# ." c ' Y'`p _ ___ _ ______ ___ ____ ___ ____ _____ ____
# / , `. w_/ | |/ / | | | _ \ \ / / \ | __ ) / _ \| __ )| ____| _ \
# jgs | '-. / / | ' /| | | | |_) \ \ /\ / / _ \ | _ \| | | | _ \| _| | |_) |
# _,..._| )_-\ \_=.\ | . \| |_| | _ < \ V V / ___ \ | |_) | |_| | |_) | |___| _ <
# `-....-'`------)))`=-'"`'" |_|\_\\___/|_| \_\ \_/\_/_/ \_\ |____/ \___/|____/|_____|_| \_\
def pipao(*args):
print(args)
class DBUtil:
def init(conn: sqlite3.Connection):
queries = [
"CREATE TABLE IF NOT EXISTS tag (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT," +
" queue_id INTEGER," +
" name TEXT" +
");",
"CREATE INDEX IF NOT EXISTS tag__queue_id ON tag(queue_id);",
"CREATE UNIQUE INDEX IF NOT EXISTS tag__queue_id_name ON tag(queue_id, name);",
"CREATE INDEX IF NOT EXISTS tag__name ON tag(name);",
"CREATE TABLE IF NOT EXISTS event (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT," +
" timestamp INTEGER," +
" comment TEXT" +
");",
"CREATE TABLE IF NOT EXISTS queue (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT," +
" name TEXT" +
");",
"CREATE TABLE IF NOT EXISTS event_tag (" +
" event_id INTEGER," +
" tag_id INTEGER" +
");",
"CREATE INDEX IF NOT EXISTS event_tag__event_id_tag_id ON event_tag(event_id, tag_id);",
"CREATE INDEX IF NOT EXISTS event_tag__event_id ON event_tag(event_id);",
"CREATE INDEX IF NOT EXISTS event_tag__tag_id ON event_tag(tag_id);",
"CREATE TABLE IF NOT EXISTS queue_event (" +
" queue_id INTEGER," +
" event_id INTEGER" +
");",
"CREATE INDEX IF NOT EXISTS queue_event__event_id_tag_id ON queue_event(queue_id, event_id);",
"CREATE INDEX IF NOT EXISTS queue_event__event_id ON queue_event(queue_id);",
"CREATE INDEX IF NOT EXISTS queue_event__tag_id ON queue_event(event_id);",
"VACUUM;"
]
for q in queries:
conn.execute(q)
def drop(file: str):
try:
os.remove(file)
except:
traceback.print_exc()
class Util:
def entity_create(
conn: sqlite3.Connection,
table: str,
key_list: list[str],
value_list: list,
) -> int:
if len(key_list) != len(value_list):
raise ValueError(f'len(key_list) != len(value_list): { len(key_list) } != { len(value_list) }')
if len(key_list) == 0:
cursor = conn.execute(f'INSERT OR IGNORE INTO { table } (id) VALUES (null)')
else:
cursor = conn.execute(f'INSERT OR IGNORE INTO { table } ({ ", ".join(key_list) }) VALUES ({ ", ".join("?" * len(key_list)) })', value_list)
if cursor.rowcount != 0:
return cursor.lastrowid
return None
def entity_delete(
conn: sqlite3.Connection,
table: str,
id: int,
):
conn.execute(f'DELETE FROM { table } WHERE id = ?', (id,))
def entity_list(
conn: sqlite3.Connection,
table: str,
):
query_result = conn.execute(f'SELECT id FROM { table }').fetchall()
if not query_result or not query_result[0]:
return []
return [ qr[0] for qr in query_result ]
def set_field(
conn: sqlite3.Connection,
table: str,
id: int,
name: str,
value
):
conn.execute(f'UPDATE { table } SET { name } = ? WHERE id = ?', (value, id,))
def get_field(
conn: sqlite3.Connection,
table: str,
id: int,
name: str
):
query_result = conn.execute(f'SELECT { name } FROM { table } WHERE id = ?', (id,)).fetchall()
if not query_result or not query_result[0]:
return None
return query_result[0][0]
def get_id_by_fields(
conn: sqlite3.Connection,
table: str,
key_list: list[str],
value_list: list,
):
query_result = conn.execute(f'SELECT id FROM { table } WHERE { " AND ".join(f"{ key } = ?" for key in key_list) }', value_list).fetchall()
if not query_result or not query_result[0]:
return None
return query_result[0][0]
def join_get(
conn: sqlite3.Connection,
table: str,
key_left: str,
key_right: str,
id_left: int,
) -> list[int]:
query_result = conn.execute(f'SELECT { key_right } FROM { table } WHERE { key_left } = ?', (id_left,)).fetchall()
if not query_result or not query_result[0]:
return []
return [ q[0] for q in query_result ]
def join_set(
conn: sqlite3.Connection,
table: str,
key_left: str,
key_right: str,
id_left: int,
id_right: int,
):
conn.execute(f'INSERT OR IGNORE INTO { table } ({ key_left }, { key_right }) VALUES (?, ?)', (id_left, id_right))
def join_delete(
conn: sqlite3.Connection,
table: str,
key_left: str,
key_right: str,
id_left: int,
id_right: int,
):
conn.execute(f'DELETE FROM { table } WHERE { key_left } = ? AND { key_right } = ?', (id_left, id_right))
def join_delete_all(
conn: sqlite3.Connection,
table: str,
key_left: str,
id_left: int,
):
conn.execute(f'DELETE FROM { table } WHERE { key_left } = ?', (id_left,))
def cleanup(
conn: sqlite3.Connection,
):
conn.executescript("""
-- Step 1: delete relations with noexistent queues
delete from queue_event where queue_id not in (select distinct id from queue);
-- Step 2: delete relations with noexistent events
delete from queue_event where event_id not in (select distinct id from event);
delete from event_tag where event_id not in (select distinct id from event);
-- Step 3: delete relations with noexistent tags
delete from event_tag where tag_id not in (select distinct id from tag);
-- Step 4: drop all unused events
delete from event where id not in (select distinct event_id from queue_event);
delete from event_tag where event_id not in (select distinct event_id from queue_event);
-- Step 5: drop all unused tags
delete from tag where id not in (select distinct tag_id from event_tag);
""")
class BaseEntity:
"""
Represents entity in database mapped to table.
Each entity has unique `id`.
"""
def __init__(
self,
conn: sqlite3.Connection,
id: int,
):
self.conn = conn
self.id = id
def get_conn(self) -> sqlite3.Connection:
return self.conn
def get_id(self) -> int:
return self.id
def get_table() -> str:
raise NotImplementedError()
class Queue(BaseEntity):
TABLE_NAME = 'queue'
def get_table(self):
return Queue.TABLE_NAME
def get_name(self) -> str:
return Util.get_field(self.get_conn(), Queue.TABLE_NAME, self.get_id(), 'name')
def set_name(self, name: str):
Util.set_field(self.get_conn(), Queue.TABLE_NAME, self.get_id(), 'name', name)
def __str__(self) -> str:
return f'Queue(id={ self.get_id() }, name="{ self.get_name() }")'
__repr__ = __str__
class Event(BaseEntity):
TABLE_NAME = 'event'
def get_table(self):
return Event.TABLE_NAME
def get_timestamp(self) -> int:
return Util.get_field(self.get_conn(), Event.TABLE_NAME, self.get_id(), 'timestamp')
def set_timestamp(self, timestamp: str):
Util.set_field(self.get_conn(), Event.TABLE_NAME, self.get_id(), 'timestamp', timestamp)
def get_comment(self) -> str:
return Util.get_field(self.get_conn(), Event.TABLE_NAME, self.get_id(), 'comment')
def set_comment(self, comment: str):
Util.set_field(self.get_conn(), Event.TABLE_NAME, self.get_id(), 'comment', comment)
def __str__(self) -> str:
return f'Event(id={ self.get_id() }, timestamp={ self.get_timestamp() }, comment="{ self.get_comment() }")'
__repr__ = __str__
class Tag(BaseEntity):
TABLE_NAME = 'tag'
def get_name(self) -> str:
return Util.get_field(self.get_conn(), Tag.TABLE_NAME, self.get_id(), 'name')
def get_queue_id(self) -> int:
return Util.get_field(self.get_conn(), Tag.TABLE_NAME, self.get_id(), 'queue_id')
def set_name(self, name: str):
Util.set_field(self.get_conn(), Tag.TABLE_NAME, self.get_id(), 'name', name)
def __str__(self) -> str:
return f'Tag(id={ self.get_id() }, name="{ self.get_name() }")'
__repr__ = __str__
class Root:
def _get_join_table(table_left: str, table_right: str) -> str:
return f'{ table_left }_{ table_right }'
def _get_id_key(table: str) -> str:
return f'{ table }_id'
def __init__(
self,
conn: sqlite3.Connection,
):
self.conn = conn
def get_conn(self) -> sqlite3.Connection:
return self.conn
def list_queue(self) -> list[Queue]:
ids = Util.entity_list(
self.conn,
Queue.TABLE_NAME,
)
return [
Queue(
self.conn,
id
)
for id in ids
]
def create_queue(self) -> Queue:
return Queue(
self.get_conn(),
Util.entity_create(
self.get_conn(),
Queue.TABLE_NAME,
[],
[],
)
)
def create_tag(self, queue: Queue, name: str) -> Tag:
id = Util.entity_create(
self.get_conn(),
Tag.TABLE_NAME,
[ Root._get_id_key(Queue.TABLE_NAME), 'name' ],
[ queue.get_id(), name ],
)
if id is None:
return None
return Tag(
self.get_conn(),
id,
)
def get_tag_by_name(self, queue: Queue, name: str) -> Tag:
id = Util.get_id_by_fields(
self.get_conn(),
Tag.TABLE_NAME,
[ 'queue_id', 'name' ],
[ queue.get_id(), name ],
)
return Tag(
self.get_conn(),
id,
)
def force_create_tag(self, queue: Queue, name: str) -> Tag:
tag = self.create_tag(queue, name)
if tag is None:
return self.get_tag_by_name(queue, name)
return tag
def list_queue_tag(self, queue: Queue) -> list[Tag]:
tag_ids = Util.join_get(
self.get_conn(),
'tag',
Root._get_id_key(Queue.TABLE_NAME),
'id',
queue.get_id(),
)
return [
Tag(
self.get_conn(),
tid,
)
for tid in tag_ids
]
def create_event(self) -> Event:
id = Util.entity_create(
self.get_conn(),
Event.TABLE_NAME,
[],
[],
)
return Event(
self.get_conn(),
id,
)
def join_queue_event(self, queue: Queue, event: Event):
Util.join_set(
self.get_conn(),
Root._get_join_table(Queue.TABLE_NAME, Event.TABLE_NAME),
Root._get_id_key(Queue.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
queue.get_id(),
event.get_id(),
)
def list_queue_event(self, queue: Queue) -> list[Event]:
ids = Util.join_get(
self.get_conn(),
Root._get_join_table(Queue.TABLE_NAME, Event.TABLE_NAME),
Root._get_id_key(Queue.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
queue.get_id(),
)
return [
Event(
self.get_conn(),
id,
)
for id in ids
]
def list_event_tag(self, event: Event) -> list[Tag]:
ids = Util.join_get(
self.get_conn(),
Root._get_join_table(Event.TABLE_NAME, Tag.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
Root._get_id_key(Tag.TABLE_NAME),
event.get_id(),
)
return [
Tag(
self.get_conn(),
id,
)
for id in ids
]
def join_event_tag(self, event: Event, tag: Tag):
Util.join_set(
self.get_conn(),
Root._get_join_table(Event.TABLE_NAME, Tag.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
Root._get_id_key(Tag.TABLE_NAME),
event.get_id(),
tag.get_id(),
)
def delete_queue_event(self, queue: Queue, event: Event):
Util.join_delete(
self.get_conn(),
Root._get_join_table(Queue.TABLE_NAME, Event.TABLE_NAME),
Root._get_id_key(Queue.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
queue.get_id(),
event.get_id(),
)
def delete_event_tag(self, event: Event, tag: Tag):
Util.join_delete(
self.get_conn(),
Root._get_join_table(Event.TABLE_NAME, Tag.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
Root._get_id_key(Tag.TABLE_NAME),
event.get_id(),
tag.get_id(),
)
def delete_event_tag_all(self, event: Event):
Util.join_delete_all(
self.get_conn(),
Root._get_join_table(Event.TABLE_NAME, Tag.TABLE_NAME),
Root._get_id_key(Event.TABLE_NAME),
event.get_id(),
)
def cleanup(self):
Util.cleanup(self.conn)