Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image DB #2

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 174 additions & 72 deletions electron/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,14 @@
</div>
</div>
</div>
</template>
<div class="flex p-6">
<Dialog>
<div
class="flex flex-wrap gap-2 items-center rounded-md border border-input bg-background text-sm w-full h-m-16 p-4">
<div class="flex p-6">
<Dialog>
<div
v-for="tag in applied_tags"
:key="tag.id"
:style="{
class="flex flex-wrap gap-2 items-center rounded-md border border-input bg-background text-sm w-full h-m-16 p-4">
<div
v-for="tag in applied_tags"
:key="tag.id"
:style="{
color: TAG_COLORS[
tag.color as keyof typeof TAG_COLORS
]?.TEXT,
Expand All @@ -116,33 +115,29 @@
tag.color as keyof typeof TAG_COLORS
]?.BORDER}`
}"
data-state="inactive"
class="flex h-6 items-center rounded bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background font-semibold text-center">
<span
class="py-1 px-2 text-sm rounded">
{{ tag.name }}
</span>
<div
@click="
applied_tags.splice(
applied_tags.indexOf(
tag
),
1
)
"
class="flex rounded bg-transparent mr-1">
<X class="w-4 h-4" />
data-state="inactive"
class="flex h-6 items-center rounded bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background font-semibold text-center">
<span
class="py-1 px-2 text-sm rounded">
{{ tag.name }}
</span>
<div
@click="
handleTagDelete(tag)
"
class="flex rounded bg-transparent mr-1">
<X class="w-4 h-4" />
</div>
</div>
<DialogTrigger asChild @click.stop>
<Plus class="w-4 h-4" />
</DialogTrigger>
<TagManager
@select-tag="handleTagSelect" />
</div>
<DialogTrigger asChild @click.stop>
<Plus class="w-4 h-4" />
</DialogTrigger>
<TagManager
@select-tag="handleTagSelect" />
</div>
</Dialog>
</div>
</Dialog>
</div>
</template>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
Expand All @@ -160,6 +155,7 @@
const tags = useState<Tag[]>('appTags');
const applied_tags = ref<Tag[]>([]);
const search_term = ref('');
const entries = useState<Entry[]>('appEntries');

async function openDialog(): Promise<string | null> {
return await window.ipcRenderer.invoke('app-open-file-dialog');
Expand Down Expand Up @@ -201,6 +197,7 @@
image: HTMLImageElement
): TagStackImageData {
return {
id: -1,
url,
width: image.width,
height: image.height,
Expand Down Expand Up @@ -228,6 +225,15 @@
const new_image_data: TagStackImageData[] = await Promise.all(
files.map(processImage)
);

for (const image of new_image_data) {
const exists = await entryExists(image);

if (!exists) await insertEntry(image);
else image.id = exists;
}

entries.value = await fetchEntries();
image_data.value = new_image_data;
} catch (error) {
console.error(
Expand All @@ -237,66 +243,162 @@
loading.value = false;
}
}
function selectImage(image: TagStackImageData) {
async function selectImage(image: TagStackImageData) {
selected_image.value = image;

if (!image) return;

const entry = entries.value.find((entry) => entry.id == image.id);

if (!entry?.fields) return;

const tag_ids: number[] =
(entry.fields as { [key: string]: any })['tag_id'] ?? [];
const tag_map: Map<number, Tag> = new Map(
tags.value.map((tag) => [tag.id, tag])
);
applied_tags.value = tag_ids
.filter((tag) => tag_map.has(tag))
.map((tag) => tag_map.get(tag) as Tag);
}
function getExtension(url: string): string {
return url.split('.').pop()!.toUpperCase();
}
function handleTagSelect(tag: Tag) {
async function handleTagSelect(tag: Tag) {
search_term.value = '';
if (!applied_tags.value.includes(tag)) applied_tags.value.push(tag);

if (!applied_tags.value.includes(tag)) {
applied_tags.value.push(tag);
if (selected_image.value)
await insertTagIntoImage(selected_image.value.id, tag.id);
}
}
onMounted(async () => {
const tags_temp = [
{
id: 0,
name: 'Archived',
aliases: ['Archive'],
color: 'RED'
},
{
id: 1,
name: 'Favorite',
aliases: ['Favorited', 'Favorites'],
color: 'YELLOW'
},
{
id: 1000,
name: 'Deferred Rendering',
shorthand: 'dr',
aliases: ['shaders'],
color: 'MINT'
}
];
await window.ipcRenderer.invoke(
function handleTagDelete(tag: Tag) {
applied_tags.value.splice(applied_tags.value.indexOf(tag), 1);

if (selected_image.value)
removeTagFromImage(selected_image.value.id, tag.id);
}
async function insertEntry(entry: TagStackImageData) {
try {
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'INSERT INTO entries (filename, path) VALUES (?, ?)',
[entry.filename, entry.directory]
);
const result = await window.ipcRenderer.invoke(
'sqlite-operations',
'get',
'SELECT MAX(id) as id FROM entries'
);
entry.id = result.id;
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'INSERT INTO fields (entry_id, tag_id) VALUES (?, ?)',
[entry.id, 1]
);
} catch (error) {
console.error('Error during insertEntry:', error);
throw new Error('Failed to insert entry.');
}
}
async function entryExists(
entry: TagStackImageData
): Promise<number | null> {
const result = await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY, name TEXT, shorthand TEXT, color TEXT)'
'get',
'SELECT id FROM entries WHERE filename = ? AND path = ?',
[entry.filename, entry.directory]
);

return result !== null && result !== undefined ? result.id : null;
}
async function getTagsFromImage(image: TagStackImageData): Promise<Tag[]> {
return await window.ipcRenderer.invoke(
'sqlite-operations',
'all',
'SELECT tags.* FROM tags JOIN fields ON tags.id = fields.tag_id JOIN entries ON fields.entry_id = entries.id WHERE entries.filename = ?',
[image.filename]
);
}
async function removeTagFromImage(id: number, tag: number) {
try {
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'DELETE FROM fields WHERE entry_id = ? AND tag_id = ?',
[id, tag]
);
entries.value = await fetchEntries();
} catch (error) {
console.error('Error removing tag from image:', error);
throw new Error('Failed to remove tag from image.');
}
}
async function insertTagIntoImage(id: number, tag: number) {
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, tag_id INTEGER, alias TEXT, UNIQUE (tag_id, alias), FOREIGN KEY (tag_id) REFERENCES tags (id))'
'INSERT INTO fields (entry_id, tag_id) VALUES (?, ?)',
[id, tag]
);

for (const tag of tags_temp) {
entries.value = await fetchEntries();
}
onMounted(async () => {
await callOnce(async () => {
const default_tags = [
{
id: 0,
name: 'Archived',
aliases: ['Archive'],
color: 'RED'
},
{
id: 1,
name: 'Favorite',
aliases: ['Favorited', 'Favorites'],
color: 'YELLOW'
}
];
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'INSERT OR REPLACE INTO tags (id, name, shorthand, color) VALUES (?, ?, ?, ?)',
[tag.id, tag.name, tag.shorthand || null, tag.color]
'CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY, name TEXT, shorthand TEXT, color TEXT)'
);

for (const alias of tag.aliases)
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, tag_id INTEGER, alias TEXT, UNIQUE (tag_id, alias), FOREIGN KEY (tag_id) REFERENCES tags (id))'
);
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'CREATE TABLE IF NOT EXISTS entries (id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, path TEXT NOT NULL)'
);
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'CREATE TABLE IF NOT EXISTS fields (entry_id INTEGER, tag_id INTEGER, FOREIGN KEY (entry_id) REFERENCES entries (id))'
);
for (const tag of default_tags) {
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'INSERT OR IGNORE INTO aliases (tag_id, alias) VALUES (?, ?)',
[tag.id, alias]
'INSERT OR REPLACE INTO tags (id, name, shorthand, color) VALUES (?, ?, ?, ?)',
[tag.id, tag.name, null, tag.color]
);
}
await callOnce(async () => {

for (const alias of tag.aliases)
await window.ipcRenderer.invoke(
'sqlite-operations',
'run',
'INSERT OR IGNORE INTO aliases (tag_id, alias) VALUES (?, ?)',
[tag.id, alias]
);
}
tags.value = await fetchTags();
});
});
Expand Down
16 changes: 16 additions & 0 deletions electron/composables/fetch_entries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const entries = ref<Entry[]>([]);

export async function fetchEntries(): Promise<Entry[]> {
const path = await window.ipcRenderer.invoke('get-user-data-path');
const fetchedEntries: any = await $fetch('/api/fetch_entries', {
query: { data: path }
});
entries.value = fetchedEntries.map((entry: Entry) => ({
id: entry.id,
filename: entry.filename,
path: entry.path,
fields: entry.fields
}));

return entries.value;
}
12 changes: 12 additions & 0 deletions electron/composables/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ export interface Tag {
color: string;
}

export interface Entry {
id: number;
filename: string;
path: string;
fields?: Field[];
}

export interface Field {
[key: string]: number[];
}

export interface FileData {
file_path: string;
file_size: string;
}

export interface TagStackImageData {
id: number;
url: string;
width: number;
height: number;
Expand Down
Loading