From 39b1e84d87ea4422ebbdab2495d242aeee231562 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Thu, 12 Jun 2025 23:52:19 +0100 Subject: [PATCH] feat: ui improvments --- .env.sample | 4 +- packages/core/src/db/schemas.ts | 5 + packages/core/src/formatters/base.ts | 7 ++ packages/core/src/formatters/predefined.ts | 2 +- packages/core/src/main.ts | 40 ++++++- packages/core/src/utils/constants.ts | 6 ++ packages/frontend/src/app/main-sidebar.tsx | 67 ++++++------ packages/frontend/src/app/page.tsx | 5 +- .../frontend/src/components/menu-content.tsx | 3 + .../frontend/src/components/menu/options.tsx | 63 +++++++++++ .../src/components/menu/save-install.tsx | 100 +++++++++++++++++- .../frontend/src/components/menu/sorting.tsx | 9 +- packages/frontend/src/context/options.tsx | 30 ++++++ 13 files changed, 298 insertions(+), 43 deletions(-) create mode 100644 packages/frontend/src/components/menu/options.tsx create mode 100644 packages/frontend/src/context/options.tsx diff --git a/.env.sample b/.env.sample index 6591f175..45363fc7 100644 --- a/.env.sample +++ b/.env.sample @@ -216,8 +216,8 @@ LOG_SENSITIVE_INFO=true # 'DEFAULT_' values are pre-filled. 'FORCE_' values override user settings. # --- Stream Proxy Enabled --- -DEFAULT_PROXY_ENABLED=true # Default state for enabling a stream proxy. -FORCE_PROXY_ENABLED=false # Force stream proxy on/off for all users. +# DEFAULT_PROXY_ENABLED=true # Default state for enabling a stream proxy. +# FORCE_PROXY_ENABLED=false # Force stream proxy on/off for all users. # --- Stream Proxy ID --- # 'mediaflow' or 'stremthru' diff --git a/packages/core/src/db/schemas.ts b/packages/core/src/db/schemas.ts index 46c3bbbe..941a287d 100644 --- a/packages/core/src/db/schemas.ts +++ b/packages/core/src/db/schemas.ts @@ -273,6 +273,11 @@ export const UserDataSchema = z.object({ includedKeywords: z.array(z.string().min(1)).optional(), excludedKeywords: z.array(z.string().min(1)).optional(), preferredKeywords: z.array(z.string().min(1)).optional(), + + randomiseResults: z.boolean().optional(), + enhanceResults: z.boolean().optional(), + enhancePosters: z.boolean().optional(), + excludedSeeders: z .object({ min: z.number().optional(), diff --git a/packages/core/src/formatters/base.ts b/packages/core/src/formatters/base.ts index 151b4b72..878db782 100644 --- a/packages/core/src/formatters/base.ts +++ b/packages/core/src/formatters/base.ts @@ -55,6 +55,7 @@ export interface ParseValue { resolution: string | null; languages: string[] | null; languageEmojis: string[] | null; + wedontknowwhatakilometeris: string[] | null; visualTags: string[] | null; audioTags: string[] | null; releaseGroup: string | null; @@ -128,6 +129,12 @@ export abstract class BaseFormatter { .map((lang) => languageToEmoji(lang) || lang) .filter((value, index, self) => self.indexOf(value) === index) : null, + wedontknowwhatakilometeris: stream.parsedFile?.languages + ? stream.parsedFile.languages + .map((lang) => languageToEmoji(lang) || lang) + .map((emoji) => emoji.replace('πŸ‡¬πŸ‡§', 'πŸ‡ΊπŸ‡ΈπŸ¦…')) + .filter((value, index, self) => self.indexOf(value) === index) + : null, visualTags: stream.parsedFile?.visualTags || null, audioTags: stream.parsedFile?.audioTags || null, releaseGroup: stream.parsedFile?.releaseGroup || null, diff --git a/packages/core/src/formatters/predefined.ts b/packages/core/src/formatters/predefined.ts index 30bd246d..00752ac8 100644 --- a/packages/core/src/formatters/predefined.ts +++ b/packages/core/src/formatters/predefined.ts @@ -47,7 +47,7 @@ export class GDriveFormatter extends BaseFormatter { super( { name: ` -{stream.proxied::istrue["πŸ•΅οΈ "||""]}{stream.type::=p2p["[P2P] "||""]}{service.shortName::exists["[{service.shortName}"||""]}{service.cached::istrue["⚑] "||""]}{service.cached::isfalse["⏳] "||""]}{addon.name}{stream.library::istrue[" (Your Media)"||""]} {stream.resolution::exists["{stream.resolution}"||""]} +{stream.proxied::istrue["πŸ•΅οΈ "||""]}{stream.type::=p2p["[P2P] "||""]}{service.shortName::exists["[{service.shortName}"||""]}{service.cached::istrue["⚑] "||""]}{service.cached::isfalse["⏳] "||""]}{addon.name}{stream.library::istrue[" (Your Media)"||""]} {stream.resolution::exists["{stream.resolution}"||""]}{stream.regexMatched::exists[" ({stream.regexMatched})"||""]} `, description: ` {stream.quality::exists["πŸŽ₯ {stream.quality} "||""]}{stream.encode::exists["🎞️ {stream.encode} "||""]}{stream.releaseGroup::exists["🏷️ {stream.releaseGroup}"||""]} diff --git a/packages/core/src/main.ts b/packages/core/src/main.ts index f3321685..0135f3aa 100644 --- a/packages/core/src/main.ts +++ b/packages/core/src/main.ts @@ -152,7 +152,9 @@ export class AIOStreams { // step 7 // proxify streaming links if a proxy is provided - const proxifiedStreams = await this.proxifyStreams(limitedStreams); + const proxifiedStreams = this.applyModifications( + await this.proxifyStreams(limitedStreams) + ); // step 8 // if this.userData.precacheNextEpisode is true, start a new thread to request the next episode, check if @@ -296,6 +298,20 @@ export class AIOStreams { } } + if (this.userData.enhancePosters) { + catalog = catalog.map((item) => { + if (Math.random() < 0.2) { + item.poster = Buffer.from( + constants.DEFAULT_POSTERS[ + Math.floor(Math.random() * constants.DEFAULT_POSTERS.length) + ], + 'base64' + ).toString('utf-8'); + } + return item; + }); + } + // step 4 return { success: true, @@ -2354,7 +2370,6 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` function keyValue(sortCriterion: SortCriterion, userData: UserData) { const { key, direction } = sortCriterion; const multiplier = direction === 'asc' ? 1 : -1; - // "quality" | "resolution" | "language" | "visualTag" | "audioTag" | "streamType" | "encode" | "size" | "service" | "seeders" | "addon" | "regexPatterns" | "cached" | "library" switch (key) { case 'cached': return ( @@ -2481,6 +2496,27 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` ); } + private applyModifications(streams: ParsedStream[]): ParsedStream[] { + if (this.userData.randomiseResults) { + streams.sort(() => Math.random() - 0.5); + } + if (this.userData.enhanceResults) { + streams.forEach((stream) => { + if (Math.random() < 0.4) { + stream.filename = undefined; + stream.parsedFile = undefined; + stream.type = 'youtube'; + stream.ytId = Buffer.from(constants.DEFAULT_YT_ID, 'base64').toString( + 'utf-8' + ); + stream.message = + 'This stream has been artificially enhanced using the best AI on the market.'; + } + }); + } + return streams; + } + private limitStreams(streams: ParsedStream[]): ParsedStream[] { if (!this.userData.resultLimits) { return streams; diff --git a/packages/core/src/utils/constants.ts b/packages/core/src/utils/constants.ts index 0039b476..1840ea6c 100644 --- a/packages/core/src/utils/constants.ts +++ b/packages/core/src/utils/constants.ts @@ -585,6 +585,12 @@ const SORT_CRITERIA = [ export const MIN_SIZE = 0; export const MAX_SIZE = 100 * 1000 * 1000 * 1000; // 100GB +export const DEFAULT_POSTERS = [ + 'aHR0cHM6Ly93d3cucG5nbWFydC5jb20vZmlsZXMvMTEvUmlja3JvbGxpbmctUE5HLVBpYy5wbmc=', +]; + +export const DEFAULT_YT_ID = 'eHZGWmpvNVBnRzA='; + export const SORT_CRITERIA_DETAILS = { quality: { name: 'Quality', diff --git a/packages/frontend/src/app/main-sidebar.tsx b/packages/frontend/src/app/main-sidebar.tsx index 7e42a6c6..41a09126 100644 --- a/packages/frontend/src/app/main-sidebar.tsx +++ b/packages/frontend/src/app/main-sidebar.tsx @@ -21,6 +21,7 @@ import { BiLogOutCircle, BiCog, BiServer, + BiSmile, } from 'react-icons/bi'; import { useRouter, usePathname } from 'next/navigation'; import { useDisclosure } from '@/hooks/disclosure'; @@ -32,6 +33,7 @@ import { Modal } from '@/components/ui/modal'; import { TextInput } from '@/components/ui/text-input'; import { toast } from 'sonner'; import { Tooltip } from '@/components/ui/tooltip'; +import { useOptions } from '@/context/options'; type MenuItem = VerticalMenuItem & { id: MenuId; @@ -43,11 +45,14 @@ export function MainSidebar() { const isCollapsed = !ctx.isBelowBreakpoint && !expandedSidebar; const { selectedMenu, setSelectedMenu } = useMenu(); const pathname = usePathname(); + const { isOptionsEnabled, enableOptions } = useOptions(); const user = useUserData(); const signInModal = useDisclosure(false); const [initialUuid, setInitialUuid] = React.useState(null); + const clickHistory = React.useRef([]); + React.useEffect(() => { const uuidMatch = pathname.match( /stremio\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/.*\/configure/ @@ -126,6 +131,16 @@ export function MainSidebar() { isCurrent: selectedMenu === 'miscellaneous', id: 'miscellaneous', }, + ...(isOptionsEnabled + ? [ + { + name: 'Fun', + iconType: BiSmile, + isCurrent: selectedMenu === 'fun', + id: 'fun', + }, + ] + : []), { name: 'Save & Install', iconType: BiSave, @@ -134,32 +149,6 @@ export function MainSidebar() { }, ]; - const bottomMenuItems: MenuItem[] = [ - ...(user.uuid && user.password - ? [ - { - name: 'Log Out', - iconType: BiLogOutCircle, - isCurrent: false, - id: 'unload-config' as MenuId, - onClick: () => { - confirmClearConfig.open(); - }, - }, - ] - : [ - { - name: 'Log In', - iconType: BiLogInCircle, - isCurrent: signInModal.isOpen, - id: 'sign-in' as MenuId, - onClick: () => { - signInModal.open(); - }, - }, - ]), - ]; - const handleExpandSidebar = () => { if (!ctx.isBelowBreakpoint && ts.expandSidebarOnHover) { setExpandSidebar(true); @@ -206,11 +195,27 @@ export function MainSidebar() {
- logo +
{ + const now = Date.now(); + const clicks = clickHistory.current.filter( + (time) => now - time < 5000 + ); + clicks.push(now); + clickHistory.current = clicks; + if (clicks.length >= 10) { + clickHistory.current = []; + enableOptions(); + } + }} + > + logo +
{status ? status.tag.includes('nightly') diff --git a/packages/frontend/src/app/page.tsx b/packages/frontend/src/app/page.tsx index 80e31f80..d6ca7dd9 100644 --- a/packages/frontend/src/app/page.tsx +++ b/packages/frontend/src/app/page.tsx @@ -21,6 +21,7 @@ import { Button } from '@/components/ui/button'; import { UserDataProvider } from '@/context/userData'; import { LuffyError } from '@/components/shared/luffy-error'; import { TextGenerateEffect } from '@/components/shared/text-generate-effect'; +import { OptionsProvider } from '@/context/options'; function ErrorOverlay({ error }: { error: string | null }) { return ( @@ -76,7 +77,9 @@ export default function Home() { - + + + diff --git a/packages/frontend/src/components/menu-content.tsx b/packages/frontend/src/components/menu-content.tsx index e3057369..a76cef19 100644 --- a/packages/frontend/src/components/menu-content.tsx +++ b/packages/frontend/src/components/menu-content.tsx @@ -10,6 +10,7 @@ import { MiscellaneousMenu } from './menu/miscellaneous'; import { SaveInstallMenu } from './menu/save-install'; import { FormatterMenu } from './menu/formatter'; import { ProxyMenu } from './menu/proxy'; +import { OptionsMenu } from './menu/options'; export function MenuContent() { const { selectedMenu } = useMenu(); @@ -33,6 +34,8 @@ export function MenuContent() { return ; case 'save-install': return ; + case 'fun': + return ; default: return (
diff --git a/packages/frontend/src/components/menu/options.tsx b/packages/frontend/src/components/menu/options.tsx new file mode 100644 index 00000000..9452fbef --- /dev/null +++ b/packages/frontend/src/components/menu/options.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { PageWrapper } from '../shared/page-wrapper'; +import { SettingsCard } from '../shared/settings-card'; +import { Switch } from '../ui/switch'; +import { useUserData } from '@/context/userData'; + +export function OptionsMenu() { + return ( + + + + ); +} + +function Content() { + const { userData, setUserData } = useUserData(); + return ( +
+
+

Options

+

{':)'}

+
+ + +
+ + setUserData((prev) => ({ + ...prev, + randomiseResults: value, + })) + } + /> + + setUserData((prev) => ({ + ...prev, + enhanceResults: value, + })) + } + /> + + setUserData((prev) => ({ + ...prev, + enhancePosters: value, + })) + } + /> +
+
+
+ ); +} diff --git a/packages/frontend/src/components/menu/save-install.tsx b/packages/frontend/src/components/menu/save-install.tsx index 8b368af8..ad13708f 100644 --- a/packages/frontend/src/components/menu/save-install.tsx +++ b/packages/frontend/src/components/menu/save-install.tsx @@ -8,10 +8,19 @@ import { PageWrapper } from '@/components/shared/page-wrapper'; import { Alert } from '@/components/ui/alert'; import { SettingsCard } from '../shared/settings-card'; import { toast } from 'sonner'; -import { DownloadIcon, UploadIcon } from 'lucide-react'; +import { CopyIcon, DownloadIcon, PlusIcon, UploadIcon } from 'lucide-react'; import { useStatus } from '@/context/status'; import { BiCopy } from 'react-icons/bi'; import { PageControls } from '../shared/page-controls'; +import { useDisclosure } from '@/hooks/disclosure'; +import { Modal } from '../ui/modal'; +import { Switch } from '../ui/switch'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '../ui/accordion'; export function SaveInstallMenu() { return ( @@ -43,6 +52,9 @@ function Content() { const baseUrl = status?.settings?.baseUrl || window.location.origin; const [addonPassword, setAddonPassword] = React.useState(''); const importFileRef = React.useRef(null); + const installModal = useDisclosure(false); + const [filterCredentialsInExport, setFilterCredentialsInExport] = + React.useState(false); React.useEffect(() => { if (userData?.addonPassword) { @@ -147,7 +159,20 @@ function Content() { const handleExport = () => { try { - const dataStr = JSON.stringify(userData, null, 2); + const dataStr = JSON.stringify( + { + ...userData, + addonPassword: filterCredentialsInExport + ? undefined + : userData.addonPassword, + services: userData?.services?.map((service) => ({ + ...service, + credentials: filterCredentialsInExport ? {} : service.credentials, + })), + }, + null, + 2 + ); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -321,7 +346,7 @@ function Content() { - @@ -346,6 +371,54 @@ function Content() {
+ */} + + + + + +
+ + + +
+
)} @@ -384,6 +457,27 @@ function Content() {
+
+ + + + Export Settings + + +
+ + setFilterCredentialsInExport(value) + } + side="right" + label="Exclude Credentials" + /> +
+
+
+
+
diff --git a/packages/frontend/src/components/menu/sorting.tsx b/packages/frontend/src/components/menu/sorting.tsx index 35594633..9ad96ef5 100644 --- a/packages/frontend/src/components/menu/sorting.tsx +++ b/packages/frontend/src/components/menu/sorting.tsx @@ -212,7 +212,10 @@ function Content() {
- +