Skip to content

Commit

Permalink
feat: add tags filter for directory entries
Browse files Browse the repository at this point in the history
  • Loading branch information
herteleo committed Oct 25, 2022
1 parent 36d7580 commit 6bf7b61
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/components/DirEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const icon = computed(() => {
</div>
<div
class="truncate rounded bg-gray-800 p-2 text-sm leading-none text-gray-500 group-hover:bg-gray-700/50"
v-text="entry.handle.name"
v-text="entry.displayName"
:title="entry.handle.name"
/>
</button>
Expand Down
8 changes: 7 additions & 1 deletion src/features/useDir.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import slugify from 'slugify';
import type router from '@/router';
import { getTagsFromString, removeTagsFromString } from '@/features/useDirFilter';

interface CurrentDirEntryBase {
handle: FileSystemHandle;
displayName: string;
tags: string[];
file: false | File;
isActive: boolean;
isDir: boolean;
Expand Down Expand Up @@ -92,6 +94,8 @@ export const setupDirEntries = async (dir: FileSystemDirectoryHandle) => {
entries.push({
handle: entry,
file,
displayName: removeTagsFromString(entry.name),
tags: getTagsFromString(entry.name),
type: getFileType(file.type, entry.name),
mime: file.type,
isActive: false,
Expand All @@ -101,6 +105,8 @@ export const setupDirEntries = async (dir: FileSystemDirectoryHandle) => {
} else {
entries.push({
handle: entry,
displayName: removeTagsFromString(entry.name),
tags: getTagsFromString(entry.name),
file: false,
type: 'dir',
isActive: false,
Expand Down
84 changes: 80 additions & 4 deletions src/features/useDirFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,108 @@ import { computed, ref, type Ref } from 'vue';
import type { CurrentDirEntry } from '@/features/useDir';

const showFilter = ref(false);
const stringFilter = ref('');

const toggleFilter = () => {
showFilter.value = !showFilter.value;
};

const stringFilter = ref('');

const tagFilter = ref<Array<string>>([]);

const isTagInFilter = (tag: string) => tagFilter.value.includes(tag);

const toggleTag = (tag: string) => {
if (isTagInFilter(tag)) {
tagFilter.value = tagFilter.value.filter((t) => t !== tag);
return;
}
tagFilter.value.push(tag);
};

const isUntaggedActive = ref(false);

const toggleUntagged = () => {
isUntaggedActive.value = !isUntaggedActive.value;
};

const resetTagFilter = () => {
tagFilter.value = [];
isUntaggedActive.value = false;
};

const resetFilter = () => {
stringFilter.value = '';
showFilter.value = false;
resetTagFilter();
};

const matchGetTags = new RegExp(/\[(.*)\](?:\.\w+)?$/);
const matchReplaceTags = new RegExp(/(\s?\[.*\])(?:\.\w+)?$/);

export const getTagsFromString = (string: string) =>
(string.match(matchGetTags)?.[1] || '')
.split(',')
.map((t) => t.trim())
.filter(Boolean)
.sort();

export const removeTagsFromString = (string: string) => {
const match = string.match(matchReplaceTags)?.[1];
return match ? string.replace(match, '').trim() : string;
};

const useDirFilter = (dirEntries: Ref<CurrentDirEntry[]>) => {
const tagFilterOptions = computed(() => {
const tags = dirEntries.value
.map(({ tags }) => tags)
.flat()
.sort();

return [...new Set([...tags, ...tagFilter.value].sort())];
});

const filteredDirEntries = computed(() => {
const string = stringFilter.value.toLowerCase();

if (!showFilter.value) return dirEntries.value;

return dirEntries.value.filter(({ handle }) => handle.name.toLowerCase().includes(string));
return dirEntries.value.filter(({ displayName, tags }) => {
const hasMatchingString = displayName.toLowerCase().includes(string);
let hasMatchingTags = false;
let show = hasMatchingString;

if (hasMatchingString && tagFilter.value.length) {
hasMatchingTags = tagFilter.value.every((t) => tags.includes(t));
show = hasMatchingTags;
}

if (hasMatchingString && isUntaggedActive.value) {
show = !tags.length || hasMatchingTags;
}

return show;
});
});

return {
showFilter,
stringFilter,
filteredDirEntries,
resetFilter,
toggleFilter,

filteredDirEntries,

stringFilter,

getTagsFromString,
isTagInFilter,
isUntaggedActive,
removeTagsFromString,
resetTagFilter,
tagFilter,
tagFilterOptions,
toggleTag,
toggleUntagged,
};
};

Expand Down
56 changes: 53 additions & 3 deletions src/views/Dir.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,24 @@ import useDirFilter from '@/features/useDirFilter';
useDir();
const { filteredDirEntries, showFilter, stringFilter, resetFilter, toggleFilter } =
useDirFilter(currentDirEntries);
const {
showFilter,
resetFilter,
toggleFilter,
filteredDirEntries,
stringFilter,
getTagsFromString,
isTagInFilter,
removeTagsFromString,
resetTagFilter,
tagFilter,
tagFilterOptions,
toggleTag,
isUntaggedActive,
toggleUntagged,
} = useDirFilter(currentDirEntries);
const $stringFilter = ref<HTMLInputElement>();
Expand Down Expand Up @@ -65,7 +81,13 @@ const dirThumbSrc = computed(
<div
class="flex items-center justify-between bg-black/75 px-4 py-1 text-gray-400 backdrop-blur"
>
<div v-text="currentDir?.name" />
<div class="flex flex-wrap items-baseline gap-4">
<span v-text="removeTagsFromString(currentDir?.name || '')" />
<span
class="text-sm text-slate-600"
v-text="getTagsFromString(currentDir?.name || '').join(', ')"
/>
</div>
<div class="flex gap-2">
<div class="animate-spin" v-if="currentDirEntriesLoading">
<app-icon class="-scale-x-100" name="AirplaneRotation" />
Expand All @@ -89,6 +111,34 @@ const dirThumbSrc = computed(
</button>
</div>
</div>
<div
v-if="showFilter && tagFilterOptions.length"
class="flex flex-wrap gap-2 bg-black/50 px-4 py-2 text-gray-400 backdrop-blur"
>
<button
v-for="tag in tagFilterOptions"
:key="tag"
class="rounded-full px-2 text-sm"
:class="isTagInFilter(tag) ? 'bg-amber-700 text-amber-100' : 'bg-slate-800 text-slate-400'"
@click="toggleTag(tag)"
v-text="tag"
/>
<button
class="rounded-full border px-2 text-sm"
:class="
isUntaggedActive ? 'border-amber-700 text-amber-300' : 'border-slate-800 text-slate-600'
"
@click="toggleUntagged()"
v-text="tagFilter.length ? '+ Untagged' : 'Untagged'"
/>
<button
v-if="tagFilter.length || isUntaggedActive"
class="text-sm text-slate-500"
@click="resetTagFilter"
>
clear
</button>
</div>
</header>
<div
v-if="!currentDirEntries.length || !filteredDirEntries.length"
Expand Down

0 comments on commit 6bf7b61

Please sign in to comment.