Skip to content

Commit ab16d1f

Browse files
refactor: update player
feat: added Feed styles feat: comment section refactor: componentify Title bar
1 parent 2cdb16d commit ab16d1f

23 files changed

+768
-141
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,12 @@ Pull Requests: [Pull Requests](https://github.com/hexadecimal233/cloudie/pulls)
7979
- [ ] Scrobbling support
8080
- [ ] DJ Support
8181
- [ ] Watch Router updates on dynamic views
82+
- [ ] Ulink is glitcing
8283
- [ ] Lyrics service
8384
- [ ] icon start to load online again...
8485
- [ ] cloudie.common.loading cloudie.common.loadMore cloudie.common.noMore cloudie.common.empty cloudie.common.emptyDesc
8586
- [ ] music title loading animation
87+
- [ ] Make tags work
88+
- [ ] add share modal
89+
- [ ] add tooltips to icon-only buttons
8690
-->

src/MainView.vue

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
11
<template>
22
<div class="mx-auto flex h-screen flex-col bg-muted relative overflow-hidden">
33
<!-- Background -->
4-
<div v-show="config.bg" class="absolute inset-0 bg-cover bg-fixed bg-center opacity-10 dark:opacity-10 background transition-all duration-700 ease-in-out z-[9999] pointer-events-none"
4+
<div v-show="config.bg" class="absolute inset-0 bg-cover bg-fixed bg-center opacity-15 dark:opacity-10 background transition-all duration-700 ease-in-out z-[9999] pointer-events-none"
55
:style="{ backgroundImage: `url(${config.bg})` }"
66
:class="{'blur-sm scale-105': config.bgBlur}"></div>
77

8-
<!-- Title Bar PS: data-tauri-drag-region doesn't work somehow -->
9-
<div class="w-full px-2 py-1 flex from-primary/5 to-secondary/5 z-10 bg-gradient-to-r" @mousedown="Window.getCurrent().startDragging()">
10-
<div class="flex items-center gap-2">
11-
<i-mingcute-moon-cloudy-line class="text-primary" />
12-
<span class="font-bold" :class="{'text-primary': windowStates.isFocused}">Cloudie</span>
13-
</div>
14-
15-
<div class="flex-1"></div>
16-
17-
<div class="flex items-center gap-2" :class="{'opacity-50': !windowStates.isFocused}" @mousedown.stop>
18-
<UButton class="cursor-pointer" icon="i-mingcute-minimize-line" color="neutral" variant="link"
19-
@click="Window.getCurrent().minimize()" />
20-
<UButton class="cursor-pointer" :icon="windowStates.isMaximized ? 'i-mingcute-restore-line' : 'i-mingcute-rectangle-line'"
21-
color="neutral" variant="link" @click="Window.getCurrent().toggleMaximize()" />
22-
<UButton class="cursor-pointer hover:bg-error hover:text-inverted" icon="i-mingcute-close-line" color="neutral"
23-
variant="link" @click="Window.getCurrent().close()" />
24-
</div>
25-
</div>
8+
<TitleBar />
269

2710
<!-- Main Area -->
2811
<div class="flex flex-1 overflow-hidden my-2">
@@ -95,7 +78,6 @@ import { useUserStore } from "@/systems/stores/user"
9578
import { getSearchSuggestions } from "@/utils/api"
9679
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"
9780
import { useColorMode, useDebounceFn } from "@vueuse/core"
98-
import { Window } from "@tauri-apps/api/window"
9981
import { config } from "./systems/config"
10082
10183
const route = useRoute()
@@ -105,21 +87,6 @@ const loading = ref(true)
10587
const colorMode = useColorMode()
10688
const userInfo = useUserStore()
10789
108-
const windowStates = ref({
109-
isFocused: false,
110-
isMaximized: false,
111-
})
112-
113-
114-
115-
Window.getCurrent().onResized(async ({}) => {
116-
windowStates.value.isMaximized = await Window.getCurrent().isMaximized()
117-
})
118-
119-
Window.getCurrent().onFocusChanged(({ payload: focused }) => {
120-
windowStates.value.isFocused = focused
121-
})
122-
12390
// 计算滚动条主题
12491
const scrollbarTheme = computed(() => {
12592
return "os-theme-" + (colorMode.value === "dark" ? "light" : "dark")

src/components/RichText.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
<template>
2-
<component :is="renderContent()" />
2+
<UContextMenu :items="items">
3+
<div>
4+
<component :is="renderContent()" />
5+
</div>
6+
</UContextMenu>
37
</template>
48

59
<script setup lang="tsx">
10+
import { computed } from "vue"
11+
import { useI18n } from "vue-i18n"
12+
13+
const items = computed(() => [
14+
{
15+
label: useI18n().t("cloudie.common.copy"),
16+
action: () => {
17+
navigator.clipboard.writeText(props.content) // FIXME: Copy fails
18+
},
19+
},
20+
])
21+
622
const props = defineProps<{
723
content: string
824
}>()
@@ -19,7 +35,7 @@ function renderContent() {
1935
<ULink
2036
key={index}
2137
to={`/user/${part.username}`}
22-
class="text-primary hover:text-primary-600 underline">
38+
class="text-primary hover:text-primary-600">
2339
@{part.username}
2440
</ULink>
2541
)
@@ -57,7 +73,7 @@ function parseContent(content: string): Array<{
5773
}> = []
5874
5975
// Regex patterns
60-
const mentionPattern = /@(\w+)/g
76+
const mentionPattern = /@([a-zA-Z0-9_-]+)/g
6177
const urlPattern = /(https?:\/\/[^\s]+)/g
6278
6379
let lastIndex = 0

src/components/TitleBar.vue

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<template>
2+
<div class="w-full px-2 py-1 flex from-primary/5 to-secondary/5 z-10 bg-gradient-to-r"
3+
@mousedown="Window.getCurrent().startDragging()">
4+
<div class="flex items-center gap-2" @mousedown.stop @click.stop="clicksFunc()">
5+
<i-mingcute-moon-cloudy-line class="text-primary" />
6+
7+
<span class="font-bold transition-none"
8+
:style="easterStyle"
9+
:class="{ 'text-primary': windowStates.isFocused && easterClicks === 0, 'spin-active': false }">
10+
Cloudie
11+
</span>
12+
</div>
13+
14+
<div class="flex-1"></div>
15+
16+
<div class="flex items-center gap-2" :class="{ 'opacity-50': !windowStates.isFocused }" @mousedown.stop>
17+
<UButton class="cursor-pointer" icon="i-mingcute-minimize-line" color="neutral" variant="link"
18+
@click="Window.getCurrent().minimize()" />
19+
<UButton class="cursor-pointer"
20+
:icon="windowStates.isMaximized ? 'i-mingcute-restore-line' : 'i-mingcute-rectangle-line'" color="neutral"
21+
variant="link" @click="Window.getCurrent().toggleMaximize()" />
22+
<UButton class="cursor-pointer hover:bg-error hover:text-inverted" icon="i-mingcute-close-line" color="neutral"
23+
variant="link" @click="Window.getCurrent().close()" />
24+
</div>
25+
</div>
26+
</template>
27+
<script setup lang="ts">
28+
import { Window } from "@tauri-apps/api/window"
29+
import { ref, computed } from "vue" // 引入 onMounted 和 onUnmounted
30+
import { useRouter } from "vue-router"
31+
32+
const clicksFunc = () => {
33+
easterClicks.value++
34+
}
35+
36+
const easterClicks = ref(0)
37+
const easterStyle = computed(() => {
38+
if (easterClicks.value === 5) {
39+
useRouter().push("/test")
40+
easterClicks.value = 0
41+
return { animation: "none" }
42+
}
43+
if (easterClicks.value === 4) {
44+
return { animation: "oldschool-blink 0.1s step-end infinite" }
45+
} else {
46+
return { animation: "none" }
47+
}
48+
})
49+
50+
const windowStates = ref({
51+
isFocused: false,
52+
isMaximized: false,
53+
})
54+
55+
Window.getCurrent().onResized(async ({}) => {
56+
windowStates.value.isMaximized = await Window.getCurrent().isMaximized()
57+
})
58+
59+
Window.getCurrent().onFocusChanged(({ payload: focused }) => {
60+
windowStates.value.isFocused = focused
61+
})
62+
</script>
63+
64+
<style>
65+
@keyframes oldschool-blink {
66+
0% { color: #000; }
67+
50% { color: #f00; }
68+
100% { color: #000; }
69+
}
70+
71+
@keyframes spin-easter-egg {
72+
from {
73+
transform: rotate(0deg);
74+
}
75+
to {
76+
transform: rotate(360deg);
77+
}
78+
}
79+
80+
.spin-active {
81+
animation: spin-easter-egg 0.5s ease-in-out infinite;
82+
transform-origin: center;
83+
}
84+
</style>

src/components/VirtualList.vue

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@
77
height: `${virtualRow.size}px`,
88
transform: `translateY(${virtualRow.start}px)`,
99
}">
10-
<slot class="w-full h-full" name="item" :item="items[virtualRow.index]" :index="virtualRow.index" />
10+
<div :ref="measureElement" class="w-full h-full">
11+
<slot name="item" :item="items[virtualRow.index]" :index="virtualRow.index" />
12+
</div>
1113
</div>
1214
</div>
1315
</div>
1416
</template>
1517

1618
<script setup lang="ts">
17-
import { ref, computed } from "vue"
19+
import { ref, computed, VNodeRef } from "vue"
1820
import { useVirtualizer } from "@tanstack/vue-virtual"
1921
2022
const props = defineProps<{
2123
items: any[]
22-
estimateSize: (index: number) => number
24+
estimateSize?: (index: number) => number
2325
}>()
2426
2527
const parentRef = ref<HTMLElement | null>(null)
@@ -30,7 +32,7 @@ const rowVirtualizer = useVirtualizer(
3032
return {
3133
count: props.items.length, // this should be reactive
3234
getScrollElement: () => parentRef.value,
33-
estimateSize: props.estimateSize,
35+
estimateSize: props.estimateSize || (() => 50), // 提供默认值
3436
overscan: 5,
3537
}
3638
}),
@@ -39,6 +41,17 @@ const rowVirtualizer = useVirtualizer(
3941
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
4042
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
4143
44+
// FIXME: dynamic height not working
45+
const measureElement = (el: any) => {
46+
if (!el) {
47+
return
48+
}
49+
50+
rowVirtualizer.value.measureElement(el)
51+
52+
return undefined
53+
}
54+
4255
function goToIndex(index: number) {
4356
rowVirtualizer.value.scrollToIndex(index, { align: "center" })
4457
}
Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,84 @@
11
<template>
2-
<div>
2+
<div class="bg-default border-t border-accented max-w-3xl mx-auto">
3+
<div class="flex flex-col gap-3">
4+
<div v-if="streamItem" class="flex items-center gap-2 text-muted">
5+
<UAvatar :src="streamItem.user.avatar_url" class="size-8" />
6+
<ULink :to="`/user/${streamItem.user.id}`" class="text-lg font-bold text-highlighted truncate">
7+
{{ streamItem.user.username }}
8+
</ULink>
9+
<i-mingcute-repeat-line v-if="streamItem.type === 'playlist-repost'" />
10+
{{ streamItem.type === 'playlist-repost' ? 'Reposted' : 'Posted' }} a playlist {{
11+
formatFromNow(streamItem.created_at)
12+
}}
13+
</div>
314

15+
<RichText v-if="streamItem?.caption" :content="streamItem.caption" class="text-sm line-clamp-1" />
16+
17+
<div class="flex items-start gap-3">
18+
<div class="relative size-28 flex-shrink-0">
19+
<img :src="replaceImageUrl(usePlaylistsStore().getCoverCache(props.playlist.id).value)"
20+
:alt="props.playlist.title" class="size-28 rounded-sm object-cover" />
21+
</div>
22+
23+
<div class="flex-1 min-w-0 flex flex-col gap-2 h-28 bg-cover bg-fixed rounded-sm bg-center px-4 pt-2">
24+
<div class="flex min-w-0 items-start justify-between">
25+
<div class="flex flex-col min-w-0 flex-1 gap-1">
26+
<UTooltip :text="props.playlist.title">
27+
<ULink :to="`/playlist/${props.playlist.id}`"
28+
class="truncate font-bold cursor-pointer max-w-full inline-block text-highlighted">
29+
{{ props.playlist.title }}
30+
</ULink>
31+
</UTooltip>
32+
<UTooltip :text="props.playlist.user.username">
33+
<ULink :to="`/user/${props.playlist.user.id}`"
34+
class="truncate text-sm text-muted cursor-pointer max-w-full inline-block">
35+
{{ props.playlist.user.username }}
36+
</ULink>
37+
</UTooltip>
38+
</div>
39+
40+
<span class="text-sm">
41+
{{ formatMillis(props.playlist.duration) }} <!-- TODO: Time display -->
42+
</span>
43+
</div>
44+
</div>
45+
</div>
46+
47+
<div class="flex max-w-xl items-center gap-3 ml-32">
48+
<UButton :icon="user.isLikedPlaylist(props.playlist.id) ? 'i-mingcute-heart-fill' : 'i-mingcute-heart-line'"
49+
:color="user.isLikedPlaylist(props.playlist.id) ? 'primary' : 'neutral'" variant="soft"
50+
:label="props.playlist.likes_count ? props.playlist.likes_count.toString() : '0'"
51+
@click="user.toggleLikePlaylist(props.playlist.id)" />
52+
<UButton
53+
:icon="user.isRepostedPlaylist(props.playlist.id) ? 'i-mingcute-share-3-fill' : 'i-mingcute-share-3-line'"
54+
:color="user.isRepostedPlaylist(props.playlist.id) ? 'primary' : 'neutral'" variant="soft"
55+
:label="props.playlist.reposts_count.toString()" @click="user.toggleRepostPlaylist(props.playlist.id)" />
56+
<UButton icon="i-mingcute-share-2-line" color="neutral" variant="soft" @click=";/* TODO: Share Track */" />
57+
</div>
58+
</div>
459
</div>
560
</template>
661

762
<script setup lang="ts">
63+
import { formatMillis, replaceImageUrl } from "@/utils/utils"
64+
import { StreamItem, UserPlaylist } from "@/utils/types"
65+
import { useUserStore } from "@/systems/stores/user"
66+
import { formatFromNow } from "@/utils/utils"
67+
import { usePlaylistsStore } from "@/systems/stores/playlists"
68+
69+
const props = defineProps<{
70+
playlist: UserPlaylist
71+
streamItem?: StreamItem
72+
}>()
873
74+
const user = useUserStore()
975
</script>
1076

1177
<style scoped>
12-
78+
/* 限制链接点击区域只包含文字内容 */
79+
:deep(a) {
80+
display: inline;
81+
width: fit-content;
82+
max-width: 100%;
83+
}
1384
</style>

0 commit comments

Comments
 (0)