Snippets: Unify Clips & Bookmarks

This commit is contained in:
Crimekillz 2024-05-01 00:31:24 +02:00
parent bacd8eb9b2
commit 6f57bce3aa
13 changed files with 263 additions and 221 deletions

View File

@ -19,6 +19,7 @@ __2024.04.1 is out!__
---
- Highlighted changes:
- Achievements adopting code from Sharkey/Misskey
- Unified Bookmarks and Clips (Snippets, dev branch)
- Chomp! Activity Support (Patch adopted [from mia](https://iceshrimp.dev/mia/withdrawal))
- Fast🚜 Scream🚜 Mod🔌 Tracker/SID playback support (Thanks to [Jeder](https://iceshrimp.dev/iceshrimp/iceshrimp/pulls/490))
- Suppress the "You haven't configured mail" message (Patch adopted [from mia](https://iceshrimp.dev/mia/withdrawal))
@ -26,15 +27,20 @@ __2024.04.1 is out!__
- Allow federation via Tor / i2p
- Allow proxying of videos and audio content (Thanks to ShittyKopper)
- Post language selection and translate functionality by Namekuji
- Profile pictures can be viewed fullscreen (dev branch)
- Accessibility settings in the unauthenticated web viewer (dev branch)
- So much more - Read the [changelog](CHANGELOG.md) to get an overview of all changes
- Planned changes:
- Unified Lists and News UI
- New central Messenger UI unifying Chats, Groups, Channels and Announcements
- Unified Exploration and Search of Users, Posts, Channels
- E2E encrypted messaging
- Improvements for single-user instances, anonymous and pseudonymous federation via Tor / i2p
- Posts Health/XP Bar to indicate how many ppl have reported a post vs. interaction rate
- Train Table Delay Captcha
- Individual session revocation and One Time Access Tokens
- Accessibility settings in the unauthenticated web viewer
- Need Ideas for:
- Better integration of User Gallery and Pages
- Don't like the Web UI? Good luck, the Mastodon-compatible API might work with the following clients:
- [Elk](https://elk.zone), [Phanpy](https://phanpy.social/), [Enafore](https://enafore.social/), [Masto-FE-standalone](https://iceshrimp.dev/iceshrimp/masto-fe-standalone) (Web)
- [Mona](https://apps.apple.com/us/app/mona-for-mastodon/id1659154653), [Toot!](https://apps.apple.com/us/app/toot-for-mastodon/id1229021451), [Ice Cubes](https://apps.apple.com/us/app/ice-cubes-for-mastodon/id6444915884), [Tusker](https://apps.apple.com/us/app/tusker/id1498334597), [Feditext](https://github.com/feditext/feditext), [Mastodon](https://apps.apple.com/us/app/mastodon-for-iphone-and-ipad/id1571998974) (iOS)

View File

@ -36,6 +36,7 @@ save: "Save"
users: "Users"
addUser: "Add a user"
addInstance: "Add a server"
snippets: "Snippets"
favorite: "Add to bookmarks"
favorites: "Bookmarks"
unfavorite: "Remove from bookmarks"

View File

@ -36,6 +36,7 @@ addUser: "ユーザーを追加"
addInstance: "サーバーを追加"
favorite: "お気に入り"
favorites: "お気に入り"
snippets: "Snippets"
unfavorite: "お気に入り解除"
favorited: "お気に入りに登録しました。"
alreadyFavorited: "既にお気に入りに登録されています。"

View File

@ -97,7 +97,7 @@ async function openSearchFilters(ev) {
},
},
{
icon: "ph-bookmark-simple ph-bold ph-lg",
icon: "ph-bookmarks ph-bold ph-lg",
text: i18n.ts._filters.inBookmarks,
action: () => {
appendSearchFilter("in:bookmarks");

View File

@ -22,7 +22,7 @@ export const navbarItemDef = reactive({
title: "messaging",
icon: "ph-tornado ph-bold ph-lg",
show: computed(() => $i != null),
indicated: computed(() => $i?.hasUnreadMessagingMessage),
indicated: computed(() => $i?.hasUnreadMessagingMessage || $i?.hasUnreadAnnouncement),
to: "/my/messaging",
},
drive: {
@ -43,12 +43,6 @@ export const navbarItemDef = reactive({
icon: "ph-hash ph-bold ph-lg",
to: "/explore",
},
announcements: {
title: "announcements",
icon: "ph-megaphone-simple ph-bold ph-lg",
indicated: computed(() => $i?.hasUnreadAnnouncement),
to: "/announcements",
},
search: {
title: "search",
icon: "ph-magnifying-glass ph-bold ph-lg",
@ -74,11 +68,11 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null),
to: "/my/antennas",
},
favorites: {
title: "favorites",
icon: "ph-bookmark-simple ph-bold ph-lg",
snippets: {
title: "snippets",
icon: "ph-bookmark ph-bold ph-lg",
show: computed(() => $i != null),
to: "/my/favorites",
to: "/my/snippets",
},
pages: {
title: "pages",
@ -90,22 +84,11 @@ export const navbarItemDef = reactive({
icon: "ph-image-square ph-bold ph-lg",
to: "/gallery",
},
clips: {
title: "clips",
icon: "ph-paperclip ph-bold ph-lg",
show: computed(() => $i != null),
to: "/my/clips",
},
channels: {
title: "channel",
icon: "ph-television ph-bold ph-lg",
to: "/channels",
},
groups: {
title: "groups",
icon: "ph-users-three ph-bold ph-lg",
to: "/my/groups",
},
achievements: {
title: "achievements",
icon: "ph-medal-military ph-bold ph-lg",

View File

@ -1,6 +1,6 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" /></template>
<template #header><MkPageHeader :actions="headerActions" :display-back-button="true" /></template>
<MkSpacer :content-max="800">
<div v-if="clip">
<div class="okzinsic _panel">

View File

@ -1,63 +0,0 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader /></template>
<MkSpacer :content-max="800">
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img
:src="instance.images.info"
class="_ghost"
alt="Info"
/>
<div>{{ i18n.ts.noNotes }}</div>
</div>
</template>
<template #default="{ items }">
<XList
v-slot="{ item }"
:items="items"
:direction="'down'"
:no-gap="false"
>
<XNote
:key="item.id"
:note="item.note"
:class="$style.note"
/>
</XList>
</template>
</MkPagination>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import MkPagination from "@/components/MkPagination.vue";
import XNote from "@/components/MkNote.vue";
import XList from "@/components/MkDateSeparatedList.vue";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import {instance} from "@/instance";
const pagination = {
endpoint: "i/favorites" as const,
limit: 10,
};
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
definePageMetadata({
title: i18n.ts.favorites,
icon: "ph-bookmark-simple ph-bold ph-lg",
});
</script>
<style lang="scss" module>
.note {
background: var(--panel);
border-radius: var(--radius);
}
</style>

View File

@ -1,118 +0,0 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
:actions="headerActions"
:tabs="headerTabs"
:display-back-button="true"
/></template>
<MkSpacer :content-max="700">
<div class="qtcaoidl">
<MkPagination
ref="pagingComponent"
:pagination="pagination"
class="list"
>
<template #empty>
<MkInfo :icon="'paperclip'" :card="true">
<p>{{ i18n.ts.clipsDesc }}</p>
</MkInfo>
</template>
<template #default="{ items }">
<MkA
v-for="item in items"
:key="item.id"
:to="`/clips/${item.id}`"
class="item _panel _gap"
>
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">
{{ item.description }}
</div>
</MkA>
</template>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import {} from "vue";
import MkPagination from "@/components/MkPagination.vue";
import MkButton from "@/components/MkButton.vue";
import MkInfo from "@/components/MkInfo.vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
const pagination = {
endpoint: "clips/list" as const,
limit: 10,
};
const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
async function create() {
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: {
type: "string",
label: i18n.ts.name,
},
description: {
type: "string",
required: false,
multiline: true,
label: i18n.ts.description,
},
isPublic: {
type: "boolean",
label: i18n.ts.public,
default: false,
},
});
if (canceled) return;
os.apiWithDialog("clips/create", result);
pagingComponent.reload();
}
function onClipCreated() {
pagingComponent.reload();
}
function onClipDeleted() {
pagingComponent.reload();
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.clip,
icon: "ph-paperclip ph-bold ph-lg",
action: {
icon: "ph-plus ph-bold ph-lg",
handler: create,
},
});
</script>
<style lang="scss" scoped>
.qtcaoidl {
> .list {
> .item {
display: block;
padding: 16px;
> .description {
margin-top: 8px;
padding-top: 8px;
border-top: solid 0.5px var(--divider);
}
}
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
/></template>
<div>
<MkSpacer :content-max="800">
<swiper
:round-lengths="true"
:touch-angle="25"
:threshold="10"
:centeredSlides="true"
:modules="[Virtual]"
:space-between="20"
:virtual="true"
:allow-touch-move="
defaultStore.state.swipeOnMobile &&
(deviceKind !== 'desktop' ||
defaultStore.state.swipeOnDesktop)
"
@swiper="setSwiperRef"
@slide-change="onSlideChange"
>
<swiper-slide>
<div class="_content qtcaoidl favorites">
<MkPagination ref="pagingComponent" :pagination="favPagination">
<template #empty>
<div class="_fullinfo">
<img
:src="instance.images.info"
class="_ghost"
alt="Info"
/>
<div>{{ i18n.ts.noNotes }}</div>
</div>
</template>
<template #default="{ items }">
<XList
v-slot="{ item }"
:items="items"
:direction="'down'"
:no-gap="false"
>
<XNote
:key="item.id"
:note="item.note"
:class="$style.note"
/>
</XList>
</template>
</MkPagination>
</div>
</swiper-slide>
<swiper-slide>
<div class="_content qtcaoidl clips">
<MkPagination
ref="pagingComponent"
:pagination="clipPagination"
class="list"
>
<template #empty>
<MkInfo :icon="'paperclip'" :card="true">
<p>{{ i18n.ts.clipsDesc }}</p>
</MkInfo>
</template>
<template #default="{ items }">
<MkA
v-for="item in items"
:key="item.id"
:to="`/clips/${item.id}`"
class="item _panel _gap"
>
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">
{{ item.description }}
</div>
</MkA>
</template>
</MkPagination>
</div>
</swiper-slide>
</swiper>
</MkSpacer>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from "vue";
import { Virtual } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/vue";
import MkPagination from "@/components/MkPagination.vue";
import MkButton from "@/components/MkButton.vue";
import MkInfo from "@/components/MkInfo.vue";
import XNote from "@/components/MkNote.vue";
import XList from "@/components/MkDateSeparatedList.vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import {instance} from "@/instance";
import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind";
import "swiper/scss";
import "swiper/scss/virtual";
const tabs = ["favorites", "clips"];
let tab = $ref(tabs[0]);
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
const MOBILE_THRESHOLD = 500;
const isMobile = ref(
deviceKind === "smartphone" || window.innerWidth <= MOBILE_THRESHOLD,
);
window.addEventListener("resize", () => {
isMobile.value =
deviceKind === "smartphone" || window.innerWidth <= MOBILE_THRESHOLD;
});
const clipPagination = {
endpoint: "clips/list" as const,
limit: 10,
};
const favPagination = {
endpoint: "i/favorites" as const,
limit: 10,
};
const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
async function create() {
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: {
type: "string",
label: i18n.ts.name,
},
description: {
type: "string",
required: false,
multiline: true,
label: i18n.ts.description,
},
isPublic: {
type: "boolean",
label: i18n.ts.public,
default: false,
},
});
if (canceled) return;
os.apiWithDialog("clips/create", result);
pagingComponent.reload();
}
function onClipCreated() {
pagingComponent.reload();
}
function onClipDeleted() {
pagingComponent.reload();
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [
{
key: "favorites",
title: i18n.ts.favorites,
icon: "ph-bookmarks ph-bold ph-lg",
},
{
key: "clips",
title: i18n.ts.clips,
icon: "ph-paperclip ph-bold ph-lg",
},
]);
let swiperRef = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(tabs.indexOf(tab));
}
function onSlideChange() {
tab = tabs[swiperRef.activeIndex];
}
function syncSlide(index) {
swiperRef.slideTo(index);
}
onMounted(() => {
syncSlide(tabs.indexOf(swiperRef.activeIndex));
});
definePageMetadata({
title: i18n.ts.snippets,
icon: "ph-bookmark ph-bold ph-lg",
action: {
icon: "ph-plus ph-bold ph-lg",
handler: create,
},
});
</script>
<style lang="scss" module>
.note {
background: var(--panel);
border-radius: var(--radius);
}
</style>
<style lang="scss" scoped>
.qtcaoidl {
> .list {
> .item {
display: block;
padding: 16px;
> .description {
margin-top: 8px;
padding-top: 8px;
border-top: solid 0.5px var(--divider);
}
}
}
}
</style>

View File

@ -88,7 +88,7 @@
v-tooltip.noDelay="i18n.ts.isAdmin"
style="color: var(--badge)"
><i
class="ph-bookmark-simple ph-fill ph-lg"
class="ph-crown-simple ph-fill ph-lg"
></i
></span>
<span
@ -96,7 +96,7 @@
v-tooltip.noDelay="i18n.ts.isModerator"
style="color: var(--badge)"
><i
class="ph-bookmark-simple ph-bold ph-lg"
class="ph-crown-simple ph-bold ph-lg"
></i
></span>
<span
@ -164,7 +164,7 @@
v-tooltip.noDelay="i18n.ts.isAdmin"
style="color: var(--badge)"
><i
class="ph-bookmark-simple ph-fill ph-lg"
class="ph-crown-simple ph-fill ph-lg"
></i
></span>
<span
@ -175,7 +175,7 @@
margin-left: 0.5rem;
"
><i
class="ph-bookmark-simple ph-bold ph-lg"
class="ph-crown-simple ph-bold ph-lg"
></i
></span>
<span

View File

@ -594,8 +594,8 @@ export const routes = [
loginRequired: true,
},
{
path: "/my/favorites",
component: page(() => import("./pages/favorites.vue")),
path: "/my/snippets",
component: page(() => import("./pages/my-snippets/index.vue")),
loginRequired: true,
},
{
@ -644,11 +644,6 @@ export const routes = [
component: page(() => import("./pages/my-lists/index.vue")),
loginRequired: true,
},
{
path: "/my/clips",
component: page(() => import("./pages/my-clips/index.vue")),
loginRequired: true,
},
{
path: "/my/groups",
component: page(() => import("./pages/my-groups/index.vue")),

View File

@ -284,12 +284,12 @@ export function getNoteMenu(props: {
statePromise.then((state) =>
state?.isFavorited
? {
icon: "ph-bookmark-simple ph-bold ph-lg",
icon: "ph-bookmarks ph-bold ph-lg",
text: i18n.ts.unfavorite,
action: () => toggleFavorite(false),
}
: {
icon: "ph-bookmark-simple ph-bold ph-lg",
icon: "ph-bookmarks ph-bold ph-lg",
text: i18n.ts.favorite,
action: () => toggleFavorite(true),
},

View File

@ -13,10 +13,13 @@ const menuOptions = [
"followRequests",
"messaging",
"-",
"favorites",
"announcements",
"snippets",
"antennas",
"explore",
"search",
"-",
"achievements",
"drive"
];
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)