mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-25 03:17:38 -07:00
[client] Add proper search page
This commit is contained in:
parent
855409332b
commit
ee4b58dee8
10 changed files with 332 additions and 276 deletions
|
@ -1131,6 +1131,7 @@ verifiedLink: "Verified link"
|
||||||
openInMainColumn: "Open in main column"
|
openInMainColumn: "Open in main column"
|
||||||
searchNotLoggedIn_1: "You have to be authenticated in order to use full text search."
|
searchNotLoggedIn_1: "You have to be authenticated in order to use full text search."
|
||||||
searchNotLoggedIn_2: "However, you can search using hashtags, and search users."
|
searchNotLoggedIn_2: "However, you can search using hashtags, and search users."
|
||||||
|
searchEmptyQuery: "Please enter a serch term."
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing
|
description: "Reduces the effort of server moderation through automatically recognizing
|
||||||
|
|
|
@ -98,15 +98,6 @@
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="input.type === 'search'" #suffix>
|
|
||||||
<button
|
|
||||||
v-tooltip.noDelay="i18n.ts.filter"
|
|
||||||
class="_buttonIcon"
|
|
||||||
@click.stop="openSearchFilters"
|
|
||||||
>
|
|
||||||
<i class="ph-funnel ph-bold"></i>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkTextarea
|
<MkTextarea
|
||||||
v-if="input && input.type === 'paragraph'"
|
v-if="input && input.type === 'paragraph'"
|
||||||
|
@ -200,15 +191,12 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
|
import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
|
||||||
import * as Acct from "iceshrimp-js/built/acct";
|
|
||||||
import MkModal from "@/components/MkModal.vue";
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import MkInput from "@/components/form/input.vue";
|
import MkInput from "@/components/form/input.vue";
|
||||||
import MkTextarea from "@/components/form/textarea.vue";
|
import MkTextarea from "@/components/form/textarea.vue";
|
||||||
import MkSelect from "@/components/form/select.vue";
|
import MkSelect from "@/components/form/select.vue";
|
||||||
import * as os from "@/os";
|
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import XSearchFilterDialog from "@/components/MkSearchFilterDialog.vue";
|
|
||||||
|
|
||||||
interface Input {
|
interface Input {
|
||||||
type: HTMLInputElement["type"];
|
type: HTMLInputElement["type"];
|
||||||
|
@ -347,179 +335,6 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateToYYYYMMDD(date) {
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
||||||
const day = ("0" + (date.getDate() + 1)).slice(-2);
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendSearchFilter(filter: string, trailingSpace: boolean = true) {
|
|
||||||
if (typeof inputValue.value !== "string") inputValue.value = "";
|
|
||||||
if (inputValue.value.length > 0 && inputValue.value.at(inputValue.value.length - 1) !== " ") inputValue.value += " ";
|
|
||||||
inputValue.value += filter;
|
|
||||||
if (trailingSpace) inputValue.value += " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openSearchFilters(ev) {
|
|
||||||
await os.popupMenu(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
icon: "ph-user ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.fromUser,
|
|
||||||
action: () => {
|
|
||||||
os.selectUser().then((user) => {
|
|
||||||
appendSearchFilter(`from:${Acct.toString(user)}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-at ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.mentioning,
|
|
||||||
action: () => {
|
|
||||||
os.selectUser().then((user) => {
|
|
||||||
appendSearchFilter(`mention:${Acct.toString(user)}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-arrow-u-up-left ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.replyTo,
|
|
||||||
action: () => {
|
|
||||||
os.selectUser().then((user) => {
|
|
||||||
appendSearchFilter(`reply:${Acct.toString(user)}`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
icon: "ph-eye ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.followingOnly,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("filter:following");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-users-three ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.followersOnly,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("filter:followers");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-link ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.fromDomain,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("instance:", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
type: "parent",
|
|
||||||
text: i18n.ts._filters.withFile,
|
|
||||||
icon: "ph-paperclip ph-bold ph-lg",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: i18n.ts.image,
|
|
||||||
icon: "ph-image-square ph-bold ph-lg",
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("has:image");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.ts.video,
|
|
||||||
icon: "ph-video-camera ph-bold ph-lg",
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("has:video");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.ts.audio,
|
|
||||||
icon: "ph-music-note ph-bold ph-lg",
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("has:audio");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.ts.file,
|
|
||||||
icon: "ph-file ph-bold ph-lg",
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("has:file");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
icon: "ph-calendar-blank ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.notesBefore,
|
|
||||||
action: () => {
|
|
||||||
os.inputDate({
|
|
||||||
title: i18n.ts._filters.notesBefore,
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.canceled) return;
|
|
||||||
appendSearchFilter("before:" + formatDateToYYYYMMDD(res.result));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-calendar-blank ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.notesAfter,
|
|
||||||
action: () => {
|
|
||||||
os.inputDate({
|
|
||||||
title: i18n.ts._filters.notesAfter,
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.canceled) return;
|
|
||||||
appendSearchFilter("after:" + formatDateToYYYYMMDD(res.result));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
icon: "ph-arrow-u-up-left ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.excludeReplies,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("-filter:replies");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-repeat ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.excludeRenotes,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("-filter:renotes");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
icon: "ph-text-aa ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.caseSensitive,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("case:sensitive");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "ph-brackets-angle ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters.matchWords,
|
|
||||||
action: () => {
|
|
||||||
appendSearchFilter("match:words");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
icon: "ph-question ph-bold ph-lg",
|
|
||||||
text: i18n.ts._filters._dialog.learnMore,
|
|
||||||
action: () => {
|
|
||||||
os.popup(XSearchFilterDialog, {}, {}, "closed");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
ev.target,
|
|
||||||
{ noReturnFocus: true },
|
|
||||||
);
|
|
||||||
inputEl.value!.focus();
|
|
||||||
inputEl.value!.selectRange((inputValue.value as string).length, (inputValue.value as string).length); // cursor at end
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener("keydown", onKeydown);
|
document.addEventListener("keydown", onKeydown);
|
||||||
});
|
});
|
||||||
|
|
239
packages/client/src/components/MkSearch.vue
Normal file
239
packages/client/src/components/MkSearch.vue
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { i18n } from "@/i18n.js";
|
||||||
|
import MkInput from "@/components/form/input.vue";
|
||||||
|
import * as os from "@/os.js";
|
||||||
|
import XSearchFilterDialog from "@/components/MkSearchFilterDialog.vue";
|
||||||
|
import { onActivated, onMounted, onUnmounted, ref, toRefs } from "vue";
|
||||||
|
import * as Acct from "iceshrimp-js/built/acct";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
query: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: "query", v: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const query = ref(props.query);
|
||||||
|
const input = ref<typeof MkInput>();
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
query.value = props.query;
|
||||||
|
input.value!.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatDateToYYYYMMDD(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = ("0" + (date.getMonth() + 1)).slice(-2);
|
||||||
|
const day = ("0" + (date.getDate() + 1)).slice(-2);
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSearchFilter(filter: string, trailingSpace: boolean = true) {
|
||||||
|
if (query.value.length > 0 && query.value.at(query.value.length - 1) !== " ") query.value += " ";
|
||||||
|
query.value += filter;
|
||||||
|
if (trailingSpace) query.value += " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSearchFilters(ev) {
|
||||||
|
await os.popupMenu(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
icon: "ph-user ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.fromUser,
|
||||||
|
action: () => {
|
||||||
|
os.selectUser().then((user) => {
|
||||||
|
appendSearchFilter(`from:${Acct.toString(user)}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-at ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.mentioning,
|
||||||
|
action: () => {
|
||||||
|
os.selectUser().then((user) => {
|
||||||
|
appendSearchFilter(`mention:${Acct.toString(user)}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-arrow-u-up-left ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.replyTo,
|
||||||
|
action: () => {
|
||||||
|
os.selectUser().then((user) => {
|
||||||
|
appendSearchFilter(`reply:${Acct.toString(user)}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: "ph-eye ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.followingOnly,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("filter:following");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-users-three ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.followersOnly,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("filter:followers");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-link ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.fromDomain,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("instance:", false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
type: "parent",
|
||||||
|
text: i18n.ts._filters.withFile,
|
||||||
|
icon: "ph-paperclip ph-bold ph-lg",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: i18n.ts.image,
|
||||||
|
icon: "ph-image-square ph-bold ph-lg",
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("has:image");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n.ts.video,
|
||||||
|
icon: "ph-video-camera ph-bold ph-lg",
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("has:video");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n.ts.audio,
|
||||||
|
icon: "ph-music-note ph-bold ph-lg",
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("has:audio");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n.ts.file,
|
||||||
|
icon: "ph-file ph-bold ph-lg",
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("has:file");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: "ph-calendar-blank ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.notesBefore,
|
||||||
|
action: () => {
|
||||||
|
os.inputDate({
|
||||||
|
title: i18n.ts._filters.notesBefore,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.canceled) return;
|
||||||
|
appendSearchFilter("before:" + formatDateToYYYYMMDD(res.result));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-calendar-blank ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.notesAfter,
|
||||||
|
action: () => {
|
||||||
|
os.inputDate({
|
||||||
|
title: i18n.ts._filters.notesAfter,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.canceled) return;
|
||||||
|
appendSearchFilter("after:" + formatDateToYYYYMMDD(res.result));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: "ph-arrow-u-up-left ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.excludeReplies,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("-filter:replies");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-repeat ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.excludeRenotes,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("-filter:renotes");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: "ph-text-aa ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.caseSensitive,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("case:sensitive");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ph-brackets-angle ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters.matchWords,
|
||||||
|
action: () => {
|
||||||
|
appendSearchFilter("match:words");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: "ph-question ph-bold ph-lg",
|
||||||
|
text: i18n.ts._filters._dialog.learnMore,
|
||||||
|
action: () => {
|
||||||
|
os.popup(XSearchFilterDialog, {}, {}, "closed");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ev.target,
|
||||||
|
{ noReturnFocus: true },
|
||||||
|
);
|
||||||
|
input.value!.focus();
|
||||||
|
input.value!.selectRange((query.value as string).length, (query.value as string).length); // cursor at end
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputKeydown(evt: KeyboardEvent) {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
emit('query', query.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkInput
|
||||||
|
class="search"
|
||||||
|
ref="input"
|
||||||
|
v-model="query"
|
||||||
|
style="flex: 1"
|
||||||
|
type="text"
|
||||||
|
autofocus
|
||||||
|
:placeholder="i18n.ts.searchPlaceholder"
|
||||||
|
:spellcheck="false"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<div>
|
||||||
|
<i class="ph-magnifying-glass ph-bold"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<button
|
||||||
|
v-tooltip.noDelay="i18n.ts.filter"
|
||||||
|
class="_buttonIcon"
|
||||||
|
@click.stop="openSearchFilters"
|
||||||
|
>
|
||||||
|
<i class="ph-funnel ph-bold"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</MkInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.search {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -43,7 +43,6 @@ import { $i, refreshAccount, login, updateAccount, signout } from "@/account";
|
||||||
import { defaultStore, ColdDeviceStorage } from "@/store";
|
import { defaultStore, ColdDeviceStorage } from "@/store";
|
||||||
import { fetchInstance, instance } from "@/instance";
|
import { fetchInstance, instance } from "@/instance";
|
||||||
import { makeHotkey } from "@/scripts/hotkey";
|
import { makeHotkey } from "@/scripts/hotkey";
|
||||||
import { search } from "@/scripts/search";
|
|
||||||
import { deviceKind } from "@/scripts/device-kind";
|
import { deviceKind } from "@/scripts/device-kind";
|
||||||
import { initializeSw } from "@/scripts/initialize-sw";
|
import { initializeSw } from "@/scripts/initialize-sw";
|
||||||
import { reloadChannel } from "@/scripts/unison-reload";
|
import { reloadChannel } from "@/scripts/unison-reload";
|
||||||
|
@ -428,7 +427,6 @@ function checkForSplash() {
|
||||||
d: (): void => {
|
d: (): void => {
|
||||||
defaultStore.set("darkMode", !defaultStore.state.darkMode);
|
defaultStore.set("darkMode", !defaultStore.state.darkMode);
|
||||||
},
|
},
|
||||||
s: search,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($i) {
|
if ($i) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { computed, ref, reactive } from "vue";
|
import { computed, ref, reactive } from "vue";
|
||||||
import { $i } from "./account";
|
import { $i } from "./account";
|
||||||
import { search } from "@/scripts/search";
|
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { ui } from "@/config";
|
import { ui } from "@/config";
|
||||||
|
@ -53,7 +52,7 @@ export const navbarItemDef = reactive({
|
||||||
search: {
|
search: {
|
||||||
title: "search",
|
title: "search",
|
||||||
icon: "ph-magnifying-glass ph-bold ph-lg",
|
icon: "ph-magnifying-glass ph-bold ph-lg",
|
||||||
action: () => search(),
|
to: "/search",
|
||||||
},
|
},
|
||||||
lists: {
|
lists: {
|
||||||
title: "lists",
|
title: "lists",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
:display-back-button="true"
|
:display-back-button="true"
|
||||||
/></template>
|
/></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
|
<MkSearch :query="query" @query="search"/>
|
||||||
<swiper
|
<swiper
|
||||||
:round-lengths="true"
|
:round-lengths="true"
|
||||||
:touch-angle="25"
|
:touch-angle="25"
|
||||||
|
@ -26,7 +27,23 @@
|
||||||
>
|
>
|
||||||
<swiper-slide>
|
<swiper-slide>
|
||||||
<template v-if="$i">
|
<template v-if="$i">
|
||||||
<XNotes ref="notes" :pagination="notesPagination" />
|
<template v-if="query == null || query.trim().length < 1">
|
||||||
|
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
|
||||||
|
<div class="_fullinfo" ref="notes">
|
||||||
|
<img
|
||||||
|
:src="instance.images.info"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Info"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{{ i18n.ts.searchEmptyQuery }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<XNotes ref="notes" :pagination="notesPagination" />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
|
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
|
||||||
|
@ -45,11 +62,27 @@
|
||||||
</template>
|
</template>
|
||||||
</swiper-slide>
|
</swiper-slide>
|
||||||
<swiper-slide>
|
<swiper-slide>
|
||||||
<XUserList
|
<template v-if="query == null || query.trim().length < 1">
|
||||||
ref="users"
|
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
|
||||||
class="_gap"
|
<div class="_fullinfo" ref="notes">
|
||||||
:pagination="usersPagination"
|
<img
|
||||||
/>
|
:src="instance.images.info"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Info"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{{ i18n.ts.searchEmptyQuery }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<XUserList
|
||||||
|
ref="users"
|
||||||
|
class="_gap"
|
||||||
|
:pagination="usersPagination"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</swiper-slide>
|
</swiper-slide>
|
||||||
</swiper>
|
</swiper>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -70,11 +103,19 @@ import { $i } from "@/account";
|
||||||
import "swiper/scss";
|
import "swiper/scss";
|
||||||
import "swiper/scss/virtual";
|
import "swiper/scss/virtual";
|
||||||
import {instance} from "@/instance";
|
import {instance} from "@/instance";
|
||||||
|
import MkSearch from "@/components/MkSearch.vue";
|
||||||
|
import { mainRouter } from "@/router.js";
|
||||||
|
import * as os from "@/os.js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
query: string;
|
defineProps<{
|
||||||
channel?: string;
|
query: string;
|
||||||
}>();
|
channel?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
query: ""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const notesPagination = {
|
const notesPagination = {
|
||||||
endpoint: "notes/search" as const,
|
endpoint: "notes/search" as const,
|
||||||
|
@ -134,8 +175,42 @@ onMounted(() => {
|
||||||
|
|
||||||
definePageMetadata(
|
definePageMetadata(
|
||||||
computed(() => ({
|
computed(() => ({
|
||||||
title: i18n.t("searchWith", { q: props.query }),
|
title: i18n.ts.search,
|
||||||
icon: "ph-magnifying-glass ph-bold ph-lg",
|
icon: "ph-magnifying-glass ph-bold ph-lg",
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function search(query: string) {
|
||||||
|
const q = query.trim();
|
||||||
|
|
||||||
|
if (q.startsWith("@") && !q.includes(" ")) {
|
||||||
|
mainRouter.push(`/${q}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q.startsWith("#")) {
|
||||||
|
mainRouter.push(`/tags/${encodeURIComponent(q.slice(1))}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q.startsWith("https://")) {
|
||||||
|
const promise = os.api("ap/show", {
|
||||||
|
uri: q,
|
||||||
|
});
|
||||||
|
|
||||||
|
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||||
|
|
||||||
|
const res = await promise;
|
||||||
|
|
||||||
|
if (res.type === "User") {
|
||||||
|
mainRouter.push(`/@${res.object.username}@${res.object.host}`);
|
||||||
|
} else if (res.type === "Note") {
|
||||||
|
mainRouter.push(`/notes/${res.object.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRouter.push(`/search?q=${encodeURIComponent(q)}`);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
import * as os from "@/os";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
import { mainRouter } from "@/router";
|
|
||||||
|
|
||||||
export async function search() {
|
|
||||||
const { canceled, result: query } = await os.inputText({
|
|
||||||
type: "search",
|
|
||||||
title: i18n.ts.search,
|
|
||||||
placeholder: i18n.ts.searchPlaceholder,
|
|
||||||
});
|
|
||||||
if (canceled || query == null || query === "") return;
|
|
||||||
|
|
||||||
const q = query.trim();
|
|
||||||
|
|
||||||
if (q.startsWith("@") && !q.includes(" ")) {
|
|
||||||
mainRouter.push(`/${q}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (q.startsWith("#")) {
|
|
||||||
mainRouter.push(`/tags/${encodeURIComponent(q.slice(1))}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// like 2018/03/12
|
|
||||||
if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, "/"))) {
|
|
||||||
const date = new Date(q.replace(/-/g, "/"));
|
|
||||||
|
|
||||||
// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
|
|
||||||
// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
|
|
||||||
// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
|
|
||||||
// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
|
|
||||||
if (q.replace(/-/g, "/").match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
|
|
||||||
date.setHours(23, 59, 59, 999);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
//v.$root.$emit('warp', date);
|
|
||||||
os.alert({
|
|
||||||
type: "waiting",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (q.startsWith("https://")) {
|
|
||||||
const promise = os.api("ap/show", {
|
|
||||||
uri: q,
|
|
||||||
});
|
|
||||||
|
|
||||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
|
||||||
|
|
||||||
const res = await promise;
|
|
||||||
|
|
||||||
if (res.type === "User") {
|
|
||||||
mainRouter.push(`/@${res.object.username}@${res.object.host}`);
|
|
||||||
} else if (res.type === "Note") {
|
|
||||||
mainRouter.push(`/notes/${res.object.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mainRouter.push(`/search?q=${encodeURIComponent(q)}`);
|
|
||||||
}
|
|
|
@ -77,7 +77,6 @@
|
||||||
import { defineAsyncComponent, defineComponent } from "vue";
|
import { defineAsyncComponent, defineComponent } from "vue";
|
||||||
import XHeader from "./header.vue";
|
import XHeader from "./header.vue";
|
||||||
import { host, instanceName } from "@/config";
|
import { host, instanceName } from "@/config";
|
||||||
import { search } from "@/scripts/search";
|
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
|
@ -118,7 +117,6 @@ export default defineComponent({
|
||||||
if (ColdDeviceStorage.get("syncDeviceDarkMode")) return;
|
if (ColdDeviceStorage.get("syncDeviceDarkMode")) return;
|
||||||
this.$store.set("darkMode", !this.$store.state.darkMode);
|
this.$store.set("darkMode", !this.$store.state.darkMode);
|
||||||
},
|
},
|
||||||
s: search,
|
|
||||||
"h|/": this.help,
|
"h|/": this.help,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -168,7 +166,7 @@ export default defineComponent({
|
||||||
help() {
|
help() {
|
||||||
// TODO(thatonecalculator): popup with keybinds
|
// TODO(thatonecalculator): popup with keybinds
|
||||||
// window.open('https://misskey-hub.net/docs/keyboard-shortcut.md', '_blank');
|
// window.open('https://misskey-hub.net/docs/keyboard-shortcut.md', '_blank');
|
||||||
console.log("d = dark/light mode, s = search, p = post :3");
|
console.log("d = dark/light mode, p = post :3");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,10 +53,10 @@
|
||||||
><i class="ph-image-square ph-bold ph-lg icon"></i
|
><i class="ph-image-square ph-bold ph-lg icon"></i
|
||||||
>{{ i18n.ts.gallery }}</MkA
|
>{{ i18n.ts.gallery }}</MkA
|
||||||
>
|
>
|
||||||
<button class="_button link" active-class="active" @click="search()">
|
<MkA to="/search" class="link" active-class="active">
|
||||||
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
|
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
|
||||||
><span>{{ i18n.ts.search }}</span>
|
><span>{{ i18n.ts.search }}</span>
|
||||||
</button>
|
</MkA>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<button class="_buttonPrimary" @click="signup()">
|
<button class="_buttonPrimary" @click="signup()">
|
||||||
{{ i18n.ts.signup }}
|
{{ i18n.ts.signup }}
|
||||||
|
@ -76,7 +76,6 @@ import { onMounted, provide } from "vue";
|
||||||
import XHeader from "./header.vue";
|
import XHeader from "./header.vue";
|
||||||
import XKanban from "./kanban.vue";
|
import XKanban from "./kanban.vue";
|
||||||
import { host, instanceName } from "@/config";
|
import { host, instanceName } from "@/config";
|
||||||
import { search } from "@/scripts/search";
|
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { instance } from "@/instance";
|
import { instance } from "@/instance";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
|
@ -123,7 +122,6 @@ const keymap = $computed(() => {
|
||||||
if (ColdDeviceStorage.get("syncDeviceDarkMode")) return;
|
if (ColdDeviceStorage.get("syncDeviceDarkMode")) return;
|
||||||
defaultStore.set("darkMode", !defaultStore.state.darkMode);
|
defaultStore.set("darkMode", !defaultStore.state.darkMode);
|
||||||
},
|
},
|
||||||
s: search,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,10 @@
|
||||||
><i class="ph-image-square ph-bold ph-lg icon"></i
|
><i class="ph-image-square ph-bold ph-lg icon"></i
|
||||||
>{{ i18n.ts.gallery }}</MkA
|
>{{ i18n.ts.gallery }}</MkA
|
||||||
>
|
>
|
||||||
<button class="_button link" active-class="active" @click="search()">
|
<MkA to="/search" class="link" active-class="active">
|
||||||
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
|
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
|
||||||
><span>{{ i18n.ts.search }}</span>
|
><span>{{ i18n.ts.search }}</span>
|
||||||
</button>
|
</MkA>
|
||||||
<div v-if="info" class="page active link">
|
<div v-if="info" class="page active link">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i v-if="info.icon" class="icon" :class="info.icon"></i>
|
<i v-if="info.icon" class="icon" :class="info.icon"></i>
|
||||||
|
@ -106,7 +106,6 @@ import XSigninDialog from "@/components/MkSigninDialog.vue";
|
||||||
import XSignupDialog from "@/components/MkSignupDialog.vue";
|
import XSignupDialog from "@/components/MkSignupDialog.vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { instance } from "@/instance";
|
import { instance } from "@/instance";
|
||||||
import { search } from "@/scripts/search";
|
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -154,8 +153,6 @@ export default defineComponent({
|
||||||
"closed",
|
"closed",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
search,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue