From 8d6c3ace0b4e4b0003d45bf6769f24308aff91a8 Mon Sep 17 00:00:00 2001 From: taskylizard <75871323+taskylizard@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:36:01 +0000 Subject: [PATCH] feat: startpage --- docker-compose.yaml | 1 - docs/.vitepress/config.mts | 6 +- docs/.vitepress/constants.ts | 5 +- docs/.vitepress/theme/components/Feedback.vue | 3 +- .../theme/components/startpage/Bookmarks.vue | 733 + .../theme/components/startpage/Clock.vue | 29 + .../theme/components/startpage/SearchBar.vue | 194 + .../theme/components/startpage/Startpage.vue | 49 + docs/.vitepress/transformer.ts | 18 +- docs/.vitepress/transformer/constants.ts | 5 +- docs/startpage.md | 11 + package.json | 5 +- pests-repellent/.vscode/settings.json | 2 +- pests-repellent/package.json | 2 +- pests-repellent/tsconfig.json | 4 +- pests-repellent/worker-configuration.d.ts | 12815 ++++++++-------- pests-repellent/wrangler.jsonc | 4 +- pnpm-lock.yaml | 20 +- unocss.config.ts | 31 +- 19 files changed, 7804 insertions(+), 6133 deletions(-) create mode 100644 docs/.vitepress/theme/components/startpage/Bookmarks.vue create mode 100644 docs/.vitepress/theme/components/startpage/Clock.vue create mode 100644 docs/.vitepress/theme/components/startpage/SearchBar.vue create mode 100644 docs/.vitepress/theme/components/startpage/Startpage.vue create mode 100644 docs/startpage.md diff --git a/docker-compose.yaml b/docker-compose.yaml index 021de6b1b..ebe39cf5b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,5 @@ services: ports: - '4173:4173' - networks: fmhy: diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 2023e7d6d..51b4ffa80 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -152,8 +152,10 @@ export default defineConfig({ search, footer: { message: `${feedback} (rev: ${commitRef})`, - copyright: `© ${new Date().getFullYear()}, Estd 2018.` + `
This site does not host any files.` - }, + copyright: + `© ${new Date().getFullYear()}, Estd 2018.` + + `
This site does not host any files.` + }, editLink: { pattern: 'https://github.com/fmhy/edit/edit/main/docs/:path', text: '📝 Edit this page' diff --git a/docs/.vitepress/constants.ts b/docs/.vitepress/constants.ts index faece2094..bf027b703 100644 --- a/docs/.vitepress/constants.ts +++ b/docs/.vitepress/constants.ts @@ -153,7 +153,10 @@ export const nav: DefaultTheme.NavItem[] = [ { text: '🔖 Bookmarks', link: 'https://github.com/fmhy/bookmarks' }, { text: '✅ SafeGuard', link: 'https://github.com/fmhy/FMHY-SafeGuard' }, { text: '📋 snowbin', link: 'https://pastes.fmhy.net' }, - { text: '®️ Redlib', link: 'https://redlib.fmhy.net/r/FREEMEDIAHECKYEAH/wiki/index' }, + { + text: '®️ Redlib', + link: 'https://redlib.fmhy.net/r/FREEMEDIAHECKYEAH/wiki/index' + }, { text: '🔎 SearXNG', link: 'https://searx.fmhy.net/' }, { text: '💡 Site Hunting', diff --git a/docs/.vitepress/theme/components/Feedback.vue b/docs/.vitepress/theme/components/Feedback.vue index 996b29456..cd4f93d7d 100644 --- a/docs/.vitepress/theme/components/Feedback.vue +++ b/docs/.vitepress/theme/components/Feedback.vue @@ -266,7 +266,8 @@ const toggleCard = () => (isCardShown.value = !isCardShown.value) placeholder="What a lovely wiki!" />

- Add your Discord handle if you would like a response, or if we need more information from you, otherwise join our + Add your Discord handle if you would like a response, or if we need + more information from you, otherwise join our +import { + DialogClose, + DialogContent, + DialogDescription, + DialogOverlay, + DialogPortal, + DialogRoot, + DialogTitle, + DialogTrigger +} from 'reka-ui' +import { useRouter } from 'vitepress' +import { onMounted, onUnmounted, ref, watch } from 'vue' + +const router = useRouter() + +export type BookmarkType = { + name: string + chord: string + url: string + icon?: string // e.g. 'i-logos:github' + color?: string + isCustom?: boolean // Track if it's a custom bookmark + customSvg?: string // Store custom SVG icon +} + +const props = defineProps<{ isInputGloballyFocused: boolean }>() + +const currentChordInput = ref('') +const activePossibleChords = ref([]) +const customBookmarks = ref([]) +const allBookmarks = ref([]) + +// Dialog states +const isAddDialogOpen = ref(false) +const isEditDialogOpen = ref(false) +const isDeleteDialogOpen = ref(false) + +// Form states +const formData = ref({ + name: '', + chord: '', + url: '', + icon: '', + color: '', + isCustom: true, + customSvg: '' +}) + +const editingBookmark = ref(null) +const deletingBookmark = ref(null) + +let chordTimeout: NodeJS.Timeout | null = null + +const initialBookmarksData: BookmarkType[] = [ + { + name: 'Hacker News', + chord: 'HN', + url: 'https://news.ycombinator.com/', + icon: 'i-logos:ycombinator' + }, + { + name: 'GitHub', + chord: 'GH', + url: 'https://github.com/', + icon: 'i-simple-icons:github' + }, + { + name: 'Reddit', + chord: 'RD', + url: 'https://reddit.com/', + icon: 'i-logos:reddit-icon' + }, + { + name: 'Twitter', + chord: 'TW', + url: 'https://twitter.com/', + icon: 'i-logos:twitter' + }, + { + name: 'YouTube', + chord: 'YT', + url: 'https://youtube.com/', + icon: 'i-logos:youtube-icon' + }, + { + name: 'Wikipedia', + chord: 'WK', + url: 'https://wikipedia.org/', + icon: 'i-simple-icons:wikipedia' + }, + { + name: "Beginner's Guide", + chord: 'BG', + url: '/beginners-guide', + icon: 'i-lucide:book-open-text' + }, + { + name: 'Wotaku', + chord: 'WT', + url: 'https://wotaku.wiki/', + icon: 'i-twemoji:flag-japan' + }, + { + name: 'privateersclub', + chord: 'PC', + url: 'https://megathread.pages.dev/', + icon: 'i-custom:privateersclub' + } +] + +// Load custom bookmarks from localStorage +const loadCustomBookmarks = () => { + try { + const stored = localStorage.getItem('customBookmarks') + if (stored) { + customBookmarks.value = JSON.parse(stored) + } + } catch (error) { + console.error('Error loading custom bookmarks:', error) + } +} + +// Save custom bookmarks to localStorage +const saveCustomBookmarks = () => { + try { + localStorage.setItem( + 'customBookmarks', + JSON.stringify(customBookmarks.value) + ) + } catch (error) { + console.error('Error saving custom bookmarks:', error) + } +} + +// Update all bookmarks when custom bookmarks change +const updateAllBookmarks = () => { + allBookmarks.value = [...initialBookmarksData, ...customBookmarks.value] +} + +// Watch for changes in custom bookmarks +watch( + customBookmarks, + () => { + updateAllBookmarks() + saveCustomBookmarks() + }, + { deep: true } +) + +const resetChord = () => { + currentChordInput.value = '' + activePossibleChords.value = [] + if (chordTimeout) clearTimeout(chordTimeout) + chordTimeout = null +} + +const handleBookmarkClick = (bookmark: BookmarkType) => { + if (bookmark.url.startsWith('/')) { + router.go(bookmark.url) + } else { + window.open(bookmark.url, '_self') + } +} + +// Form validation +const isFormValid = () => { + return ( + formData.value.name.trim() && + formData.value.chord.trim() && + formData.value.url.trim() && + !isChordTaken(formData.value.chord, editingBookmark.value?.chord) + ) +} + +const isChordTaken = (chord: string, excludeChord?: string) => { + return allBookmarks.value.some( + (b) => + b.chord.toUpperCase() === chord.toUpperCase() && b.chord !== excludeChord + ) +} + +// Reset form +const resetForm = () => { + formData.value = { + name: '', + chord: '', + url: '', + icon: '', + color: '', + isCustom: true, + customSvg: '' + } +} + +// Add bookmark +const handleAddBookmark = () => { + if (!isFormValid()) return + + const newBookmark: BookmarkType = { + ...formData.value, + chord: formData.value.chord.toUpperCase(), + isCustom: true + } + + // If no icon and no custom SVG, use default website icon + if (!newBookmark.icon && !newBookmark.customSvg) { + newBookmark.icon = 'i-lucide:globe' + } + + customBookmarks.value.push(newBookmark) + isAddDialogOpen.value = false + resetForm() +} + +// Edit bookmark +const openEditDialog = (bookmark: BookmarkType) => { + editingBookmark.value = bookmark + formData.value = { ...bookmark } + isEditDialogOpen.value = true +} + +const handleEditBookmark = () => { + if (!isFormValid() || !editingBookmark.value) return + + const index = customBookmarks.value.findIndex( + (b) => b === editingBookmark.value + ) + if (index !== -1) { + customBookmarks.value[index] = { + ...formData.value, + chord: formData.value.chord.toUpperCase(), + isCustom: true + } + + // If no icon and no custom SVG, use default website icon + if ( + !customBookmarks.value[index].icon && + !customBookmarks.value[index].customSvg + ) { + customBookmarks.value[index].icon = 'i-lucide:globe' + } + } + + isEditDialogOpen.value = false + editingBookmark.value = null + resetForm() +} + +// Delete bookmark +const openDeleteDialog = (bookmark: BookmarkType) => { + deletingBookmark.value = bookmark + isDeleteDialogOpen.value = true +} + +const handleDeleteBookmark = () => { + if (!deletingBookmark.value) return + + const index = customBookmarks.value.findIndex( + (b) => b === deletingBookmark.value + ) + if (index !== -1) { + customBookmarks.value.splice(index, 1) + } + + isDeleteDialogOpen.value = false + deletingBookmark.value = null +} + +// Handle SVG input +const handleSvgInput = (event: Event) => { + const target = event.target as HTMLTextAreaElement + formData.value.customSvg = target.value +} + +const handleKeyDown = (e: KeyboardEvent) => { + if ( + props.isInputGloballyFocused || + e.altKey || + e.metaKey || + e.ctrlKey || + e.shiftKey + ) + return + + const active = document.activeElement as HTMLElement | null + if ( + active?.tagName === 'INPUT' || + active?.tagName === 'TEXTAREA' || + active?.isContentEditable + ) + return + + const key = e.key.toUpperCase() + if (chordTimeout) clearTimeout(chordTimeout) + + if (!currentChordInput.value) { + const matches = allBookmarks.value.filter((b) => b.chord.startsWith(key)) + if (matches.length) { + e.preventDefault() + currentChordInput.value = key + activePossibleChords.value = matches + chordTimeout = setTimeout(resetChord, 2000) + } + } else { + const next = currentChordInput.value + key + const match = activePossibleChords.value.find((b) => b.chord === next) + if (match) { + if (match.url.startsWith('/')) { + router.go(match.url) + } else { + window.open(match.url, '_self') + } + resetChord() + } else { + const filtered = allBookmarks.value.filter((b) => + b.chord.startsWith(next) + ) + if (filtered.length) { + currentChordInput.value = next + activePossibleChords.value = filtered + chordTimeout = setTimeout(resetChord, 2000) + } else { + resetChord() + } + } + } +} + +onMounted(() => { + loadCustomBookmarks() + updateAllBookmarks() + document.addEventListener('keydown', handleKeyDown) +}) + +onUnmounted(() => { + document.removeEventListener('keydown', handleKeyDown) + if (chordTimeout) clearTimeout(chordTimeout) +}) + + +