diff --git a/.env.example b/.env.example deleted file mode 100644 index 182fea9bf..000000000 --- a/.env.example +++ /dev/null @@ -1,54 +0,0 @@ -# ============================================== -# Mana Core Auth - Environment Variables -# ============================================== - -# Application -NODE_ENV=production -PORT=3001 - -# Database (PostgreSQL) -POSTGRES_DB=manacore -POSTGRES_USER=manacore -POSTGRES_PASSWORD=your-secure-postgres-password-here - -# Full database URL (used by app) -DATABASE_URL=postgresql://manacore:your-secure-postgres-password-here@pgbouncer:6432/manacore - -# Redis -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_PASSWORD=your-secure-redis-password-here - -# JWT Configuration -# Generate RS256 key pair: -# openssl genrsa -out private.pem 2048 -# openssl rsa -in private.pem -pubout -out public.pem -JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nYOUR_PUBLIC_KEY_HERE\n-----END PUBLIC KEY-----" -JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END RSA PRIVATE KEY-----" -JWT_ACCESS_TOKEN_EXPIRY=15m -JWT_REFRESH_TOKEN_EXPIRY=7d -JWT_ISSUER=manacore -JWT_AUDIENCE=manacore - -# Stripe -STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key -STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key -STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret - -# CORS -CORS_ORIGINS=http://localhost:3000,http://localhost:8081,https://yourdomain.com - -# Traefik / SSL -ACME_EMAIL=your-email@example.com -AUTH_DOMAIN=auth.yourdomain.com - -# Credits Configuration -CREDITS_SIGNUP_BONUS=150 -CREDITS_DAILY_FREE=5 - -# Monitoring -GRAFANA_ADMIN_PASSWORD=your-secure-grafana-password - -# Rate Limiting -RATE_LIMIT_TTL=60 -RATE_LIMIT_MAX=100 diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index c7d18100b..000000000 --- a/.prettierrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "plugins": ["prettier-plugin-svelte", "prettier-plugin-astro"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - }, - { - "files": "*.astro", - "options": { - "parser": "astro" - } - } - ] -} diff --git a/CLAUDE.md b/CLAUDE.md index 8accf5776..60d1dc9c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,21 +43,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/ ### Archived Projects (`apps-archived/`) -These projects are temporarily archived and excluded from the workspace. To re-activate, move back to `apps/`. - -| Project | Description | -| ------------------ | -------------------------------- | -| **bauntown** | Community website for developers | -| **memoro** | Voice memo & AI analysis | -| **news** | News aggregation | -| **nutriphi** | Nutrition tracking | -| **reader** | Reading app | -| **uload** | URL shortener | -| **wisekeep** | AI wisdom extraction from video | -| **techbase** | Software comparison platform | -| **inventory** | Inventory management | -| **presi** | Presentation tool | -| **storage** | Cloud storage | +Currently empty. To archive a project, move it from `apps/` to `apps-archived/` (excluded from workspace). ## Development Commands diff --git a/README.md b/README.md index d3ff122b7..50be1c544 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,29 @@ # Manacore Monorepo -Monorepo containing all Manacore projects with shared packages and unified tooling. +Monorepo containing all Manacore projects — a self-hosted multi-app ecosystem with shared packages and unified tooling. ## Projects -| Project | Description | Tech Stack | -| ------------------ | ------------------------------- | ------------------------------ | -| **maerchenzauber** | AI-powered story generation app | NestJS, Expo, SvelteKit, Astro | -| **manacore** | Multi-app ecosystem platform | Expo, SvelteKit, Astro | -| **manadeck** | Card/deck management app | NestJS, Expo, SvelteKit | -| **memoro** | Voice memo & AI analysis app | Expo, SvelteKit, Astro | +| Project | Description | Apps | +|---------|-------------|------| +| **manacore** | Multi-app ecosystem platform | Expo mobile, SvelteKit web | +| **chat** | AI chat application | NestJS backend, Expo mobile, SvelteKit web, Astro landing | +| **todo** | Task management | NestJS backend, SvelteKit web, Astro landing | +| **calendar** | Calendar & scheduling | NestJS backend, SvelteKit web, Astro landing | +| **clock** | Pomodoro & time tracking | NestJS backend, SvelteKit web, Astro landing | +| **contacts** | Contact management | NestJS backend, SvelteKit web | +| **picture** | AI image generation | NestJS backend, Expo mobile, SvelteKit web, Astro landing | +| **manadeck** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web | +| **zitare** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing | +| **mukke** | Music player | NestJS backend, SvelteKit web | +| **planta** | Plant care tracker | NestJS backend, SvelteKit web | +| **storage** | Cloud storage | NestJS backend, SvelteKit web | +| **questions** | Q&A with web search | SvelteKit web | +| **skilltree** | Skill tree visualization | NestJS backend, SvelteKit web | +| **nutriphi** | Nutrition tracking | NestJS backend, SvelteKit web | +| **citycorners** | City guide | NestJS backend, SvelteKit web, Astro landing | +| **presi** | Presentation tool | NestJS backend, SvelteKit web | +| **photos** | Photo management | NestJS backend, SvelteKit web | ## Getting Started @@ -17,107 +31,52 @@ Monorepo containing all Manacore projects with shared packages and unified tooli - Node.js 20+ - pnpm 9.15.0+ +- Docker (for PostgreSQL, Redis, MinIO) ### Installation ```bash -# Install pnpm globally (if not installed) -npm install -g pnpm - -# Install all dependencies pnpm install ``` ### Development ```bash -# Start all projects in dev mode -pnpm run dev +# Start infrastructure (PostgreSQL, Redis, MinIO) +pnpm docker:up -# Start a specific project -pnpm run maerchenzauber:dev -pnpm run manacore:dev -pnpm run manadeck:dev -pnpm run memoro:dev +# Start any app with auto DB setup +pnpm dev:chat:full +pnpm dev:todo:full +pnpm dev:calendar:full +pnpm dev:contacts:full -# Build all projects +# Build & quality pnpm run build - -# Run tests -pnpm run test - -# Type check pnpm run type-check - -# Format code pnpm run format ``` -## Shared Packages +See [CLAUDE.md](./CLAUDE.md) for comprehensive development documentation. -Located in `packages/`: - -| Package | Description | -| --------------------------- | --------------------------------------- | -| `@manacore/shared-types` | Common TypeScript types | -| `@manacore/shared-supabase` | Unified Supabase client | -| `@manacore/shared-utils` | Utility functions (date, string, async) | -| `@manacore/shared-ui` | React Native UI components | - -### Using Shared Packages - -```typescript -// In any project -import { User, ApiResponse } from '@manacore/shared-types'; -import { createSupabaseClient } from '@manacore/shared-supabase'; -import { formatDate, truncate, retry } from '@manacore/shared-utils'; -``` - -## Repository Structure +## Architecture ``` manacore-monorepo/ -├── packages/ # Shared packages -│ ├── shared-types/ # TypeScript types -│ ├── shared-supabase/ # Supabase utilities -│ ├── shared-utils/ # Common utilities -│ └── shared-ui/ # React Native components -├── maerchenzauber/ # Storyteller project -├── manacore/ # Manacore apps project -├── manadeck/ # ManaDeck project -├── memoro/ # Memoro project -├── turbo.json # Turborepo configuration -├── pnpm-workspace.yaml # Workspace configuration -└── package.json # Root package +├── apps/ # Product applications +├── services/ # Microservices (auth, search, LLM, bots) +├── packages/ # Shared packages +├── docker/ # Docker configuration +└── scripts/ # Development & deployment scripts ``` ## Tooling - **Package Manager:** pnpm 9.15.0 - **Build System:** Turborepo -- **Formatting:** Prettier -- **Node Version:** 20 (see .nvmrc) - -## Adding Dependencies - -```bash -# Add to root (dev tools) -pnpm add -D -w - -# Add to specific project -pnpm add --filter maerchenzauber - -# Add to shared package -pnpm add --filter @manacore/shared-utils -``` - -## Contributing - -1. Create a feature branch -2. Make changes -3. Run `pnpm run format` and `pnpm run type-check` -4. Commit with conventional commit messages -5. Create pull request +- **Formatting:** Prettier (tabs, single quotes, 100 char width) +- **Hosting:** Mac Mini (self-hosted) via Docker + Cloudflare Tunnel +- **Analytics:** Umami (stats.mana.how) ## License diff --git a/apps-archived/mukke/CLAUDE.md b/apps-archived/mukke/CLAUDE.md deleted file mode 100644 index b586e5bbe..000000000 --- a/apps-archived/mukke/CLAUDE.md +++ /dev/null @@ -1,60 +0,0 @@ -# CLAUDE.md - Mukke - -Offline-first iOS Music Player. Songs aus iCloud/lokalen Dateien importieren, lokal auf dem Gerät speichern, abspielen. - -## Project Structure - -``` -apps/mukke/ -├── package.json # Orchestrator (name: mukke) -├── apps/ -│ └── mobile/ # @mukke/mobile (Expo SDK 54) -│ ├── app/ # Expo Router screens -│ │ ├── (tabs)/ # 4 Tab-Screens (Bibliothek, Playlists, Suche, Settings) -│ │ ├── player.tsx # Full-Screen Player (modal) -│ │ ├── queue.tsx # Queue Ansicht (modal) -│ │ ├── album/[id] # Album Detail -│ │ ├── artist/[id] # Artist Detail -│ │ └── playlist/ # Playlist Detail + New -│ ├── components/ # UI components -│ ├── contexts/ # AudioContext (expo-audio) -│ ├── stores/ # Zustand stores (player, library, playlist) -│ ├── services/ # Business logic (DB, import, audio, library, playlist) -│ └── utils/ # Theme system -└── packages/ - └── mukke-types/ # @mukke/types (shared interfaces) -``` - -## Commands - -```bash -pnpm dev:mukke:mobile # Start Expo app -``` - -## Tech Stack - -- **Audio**: expo-audio (background via UIBackgroundModes: ["audio"]) -- **Import**: expo-document-picker (iCloud + lokale Dateien) -- **Storage**: expo-file-system (documentDirectory) -- **Metadata**: @missingcore/audio-metadata (ID3v2.3/v2.4) -- **DB**: expo-sqlite (SQLite für Songs, Playlists) -- **State**: Zustand -- **Navigation**: Expo Router + NativeTabs -- **Styling**: NativeWind / Tailwind - -## Architecture - -- **No backend** - pure offline, local-only app -- **SQLite** for structured data (songs, playlists, playlist_songs) -- **Albums/Artists/Genres** derived from songs table via queries (no separate tables) -- **File storage**: documentDirectory/music/ + documentDirectory/artwork/ -- **Audio playback**: expo-audio with background mode -- **MiniPlayer**: persistent above tab bar - -## Import Flow - -1. User taps Import → expo-document-picker opens (iCloud + local) -2. Files copied to documentDirectory/music/{uuid}.ext -3. Metadata extracted via @missingcore/audio-metadata -4. Cover art saved to documentDirectory/artwork/{uuid}.jpg -5. Song entry created in SQLite diff --git a/apps-archived/mukke/apps/mobile/.gitignore b/apps-archived/mukke/apps/mobile/.gitignore deleted file mode 100644 index 5873d9abc..000000000 --- a/apps-archived/mukke/apps/mobile/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ - -# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb -# The following patterns were generated by expo-cli - -expo-env.d.ts -# @end expo-cli \ No newline at end of file diff --git a/apps-archived/mukke/apps/mobile/app.json b/apps-archived/mukke/apps/mobile/app.json deleted file mode 100644 index ec396f149..000000000 --- a/apps-archived/mukke/apps/mobile/app.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "expo": { - "name": "Mukke", - "slug": "mukke", - "version": "1.0.0", - "scheme": "mukke", - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./assets/favicon.png" - }, - "plugins": ["expo-router", "expo-sqlite"], - "experiments": { - "typedRoutes": true, - "tsconfigPaths": true - }, - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "automatic", - "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "assetBundlePatterns": ["**/*"], - "ios": { - "supportsTablet": true, - "bundleIdentifier": "com.mana.mukke", - "infoPlist": { - "UIBackgroundModes": ["audio"] - }, - "config": { - "usesNonExemptEncryption": false - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" - }, - "package": "com.mana.mukke" - }, - "owner": "memoro", - "extra": { - "router": { - "origin": false - }, - "eas": { - "projectId": "placeholder" - } - } - } -} diff --git a/apps-archived/mukke/apps/mobile/app/(tabs)/_layout.tsx b/apps-archived/mukke/apps/mobile/app/(tabs)/_layout.tsx deleted file mode 100644 index b088e9b55..000000000 --- a/apps-archived/mukke/apps/mobile/app/(tabs)/_layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; -import { View } from 'react-native'; - -import { MiniPlayer } from '~/components/MiniPlayer'; - -export default function TabLayout() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/(tabs)/index.tsx b/apps-archived/mukke/apps/mobile/app/(tabs)/index.tsx deleted file mode 100644 index 6a099d7cf..000000000 --- a/apps-archived/mukke/apps/mobile/app/(tabs)/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Stack } from 'expo-router'; -import { useEffect } from 'react'; -import { View } from 'react-native'; - -import { AlbumGrid } from '~/components/AlbumGrid'; -import { ArtistList } from '~/components/ArtistList'; -import { GenreList } from '~/components/GenreList'; -import { ImportButton } from '~/components/ImportButton'; -import { SegmentedControl } from '~/components/SegmentedControl'; -import { SongList } from '~/components/SongList'; -import { SortMenu } from '~/components/SortMenu'; -import { useLibraryStore } from '~/stores/libraryStore'; -import type { LibraryTab } from '~/types'; - -const SEGMENTS: { key: LibraryTab; label: string }[] = [ - { key: 'songs', label: 'Songs' }, - { key: 'albums', label: 'Alben' }, - { key: 'artists', label: 'Künstler' }, - { key: 'genres', label: 'Genres' }, -]; - -export default function LibraryScreen() { - const { - songs, - albums, - artists, - genres, - activeTab, - sortField, - sortDirection, - setActiveTab, - setSortField, - setSortDirection, - loadAll, - } = useLibraryStore(); - - useEffect(() => { - loadAll(); - }, []); - - return ( - - ( - - {activeTab === 'songs' && ( - { - setSortField(field); - setSortDirection(dir); - }} - /> - )} - - - ), - }} - /> - - - - {activeTab === 'songs' && } - {activeTab === 'albums' && } - {activeTab === 'artists' && } - {activeTab === 'genres' && } - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/(tabs)/playlists.tsx b/apps-archived/mukke/apps/mobile/app/(tabs)/playlists.tsx deleted file mode 100644 index 04a7128a2..000000000 --- a/apps-archived/mukke/apps/mobile/app/(tabs)/playlists.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Stack, useRouter } from 'expo-router'; -import { useEffect } from 'react'; -import { FlatList, Pressable, View, Text } from 'react-native'; - -import { EmptyState } from '~/components/EmptyState'; -import { ListItem } from '~/components/ListItem'; -import { usePlaylistStore } from '~/stores/playlistStore'; -import { useTheme } from '~/utils/themeContext'; - -export default function PlaylistsScreen() { - const { colors } = useTheme(); - const router = useRouter(); - const { playlists, loadPlaylists } = usePlaylistStore(); - - useEffect(() => { - loadPlaylists(); - }, []); - - return ( - - ( - router.push('/playlist/new')} style={{ padding: 8 }}> - - - ), - }} - /> - - {playlists.length === 0 ? ( - - ) : ( - item.id} - contentContainerStyle={{ paddingBottom: 100 }} - renderItem={({ item }) => ( - - - - } - onPress={() => router.push(`/playlist/${item.id}`)} - showChevron - /> - )} - /> - )} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/(tabs)/search.tsx b/apps-archived/mukke/apps/mobile/app/(tabs)/search.tsx deleted file mode 100644 index d932d9730..000000000 --- a/apps-archived/mukke/apps/mobile/app/(tabs)/search.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Stack } from 'expo-router'; -import { useState, useCallback } from 'react'; -import { View, TextInput } from 'react-native'; - -import { EmptyState } from '~/components/EmptyState'; -import { SongList } from '~/components/SongList'; -import { searchSongs } from '~/services/libraryService'; -import type { Song } from '~/types'; -import { useTheme } from '~/utils/themeContext'; - -export default function SearchScreen() { - const { colors } = useTheme(); - const [query, setQuery] = useState(''); - const [results, setResults] = useState([]); - const [hasSearched, setHasSearched] = useState(false); - - const handleSearch = useCallback(async (text: string) => { - setQuery(text); - if (text.trim().length < 2) { - setResults([]); - setHasSearched(false); - return; - } - setHasSearched(true); - const songs = await searchSongs(text.trim()); - setResults(songs); - }, []); - - return ( - - - - - - - - - {!hasSearched ? ( - - ) : results.length === 0 ? ( - - ) : ( - - )} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/(tabs)/settings.tsx b/apps-archived/mukke/apps/mobile/app/(tabs)/settings.tsx deleted file mode 100644 index d4d53a307..000000000 --- a/apps-archived/mukke/apps/mobile/app/(tabs)/settings.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Stack } from 'expo-router'; -import { useEffect, useState } from 'react'; -import { View, Text, Switch, ScrollView, Alert, Pressable } from 'react-native'; - -import { useTheme, type ThemeVariant } from '~/utils/themeContext'; -import { getStorageInfo, formatFileSize } from '~/services/fileService'; -import { getSongCount } from '~/services/libraryService'; -import { pickAndImportFiles } from '~/services/importService'; -import { useLibraryStore } from '~/stores/libraryStore'; - -export default function SettingsScreen() { - const { colors, isDarkMode, toggleTheme, themeVariant, setThemeVariant } = useTheme(); - const loadAll = useLibraryStore((s) => s.loadAll); - const [storageInfo, setStorageInfo] = useState({ musicSize: 0, artworkSize: 0, totalFiles: 0 }); - const [songCount, setSongCount] = useState(0); - - useEffect(() => { - loadInfo(); - }, []); - - const loadInfo = async () => { - const [storage, count] = await Promise.all([getStorageInfo(), getSongCount()]); - setStorageInfo(storage); - setSongCount(count); - }; - - const handleImport = async () => { - try { - const songs = await pickAndImportFiles(); - if (songs.length > 0) { - await loadAll(); - await loadInfo(); - Alert.alert('Importiert', `${songs.length} Song${songs.length > 1 ? 's' : ''} importiert.`); - } - } catch (error) { - Alert.alert('Fehler', 'Beim Import ist ein Fehler aufgetreten.'); - } - }; - - const variants: { key: ThemeVariant; label: string; color: string }[] = [ - { key: 'classic', label: 'Orange', color: '#FF6B35' }, - { key: 'ocean', label: 'Blau', color: '#2196F3' }, - { key: 'sunset', label: 'Rot', color: '#FF6B6B' }, - ]; - - return ( - - - - {/* Appearance */} - - Darstellung - - - - Dark Mode - - - - - Akzentfarbe - - {variants.map((v) => ( - setThemeVariant(v.key)} - style={{ - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: v.color, - borderWidth: themeVariant === v.key ? 3 : 0, - borderColor: colors.text, - }} - /> - ))} - - - - - {/* Import */} - - Musik - - - - - Songs importieren - - - - {/* Storage */} - - Speicher - - - - - Songs - {songCount} - - - Musik - - {formatFileSize(storageInfo.musicSize)} - - - - Cover Art - - {formatFileSize(storageInfo.artworkSize)} - - - - - - {/* About */} - - Info - - - - - Version - 1.0.0 - - - Mukke - Offline Music Player - - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/+not-found.tsx b/apps-archived/mukke/apps/mobile/app/+not-found.tsx deleted file mode 100644 index bb486b5ef..000000000 --- a/apps-archived/mukke/apps/mobile/app/+not-found.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Link, Stack } from 'expo-router'; -import { Text, View } from 'react-native'; - -export default function NotFoundScreen() { - return ( - <> - - - Diese Seite existiert nicht. - - Zur Bibliothek - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/_layout.tsx b/apps-archived/mukke/apps/mobile/app/_layout.tsx deleted file mode 100644 index 1719ff3c1..000000000 --- a/apps-archived/mukke/apps/mobile/app/_layout.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import '../global.css'; -import { Stack } from 'expo-router'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; - -import { ThemeWrapper } from '~/components/ThemeWrapper'; -import { AudioProvider } from '~/contexts/AudioContext'; -import { ThemeProvider } from '~/utils/themeContext'; - -export const unstable_settings = { - initialRouteName: '(tabs)', -}; - -export default function RootLayout() { - return ( - - - - {({ isDarkMode }) => ( - - - - - - - - - - - - - - )} - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/album/[id].tsx b/apps-archived/mukke/apps/mobile/app/album/[id].tsx deleted file mode 100644 index 23e7ebed7..000000000 --- a/apps-archived/mukke/apps/mobile/app/album/[id].tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useLocalSearchParams } from 'expo-router'; -import { useEffect, useState } from 'react'; -import { View, Text } from 'react-native'; - -import { Artwork } from '~/components/Artwork'; -import { SongList } from '~/components/SongList'; -import { getSongsByAlbum } from '~/services/libraryService'; -import { usePlayerStore } from '~/stores/playerStore'; -import type { Song } from '~/types'; -import { useTheme } from '~/utils/themeContext'; - -export default function AlbumDetailScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); - const albumName = decodeURIComponent(id || ''); - const { colors } = useTheme(); - const playSong = usePlayerStore((s) => s.playSong); - const [songs, setSongs] = useState([]); - - useEffect(() => { - if (albumName) { - getSongsByAlbum(albumName).then(setSongs); - } - }, [albumName]); - - const coverArt = songs.find((s) => s.coverArtPath)?.coverArtPath || null; - const artist = songs[0]?.albumArtist || songs[0]?.artist || 'Unbekannt'; - const year = songs[0]?.year; - - return ( - - {/* Album Header */} - - - - {albumName} - - - {artist} - {year ? ` · ${year}` : ''} · {songs.length} Songs - - - - playSong(song, songs, index)} - emptyTitle="Keine Songs" - /> - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/artist/[id].tsx b/apps-archived/mukke/apps/mobile/app/artist/[id].tsx deleted file mode 100644 index 5d56e0abc..000000000 --- a/apps-archived/mukke/apps/mobile/app/artist/[id].tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useLocalSearchParams } from 'expo-router'; -import { useEffect, useState } from 'react'; -import { View, Text } from 'react-native'; - -import { Artwork } from '~/components/Artwork'; -import { SongList } from '~/components/SongList'; -import { getSongsByArtist } from '~/services/libraryService'; -import { usePlayerStore } from '~/stores/playerStore'; -import type { Song } from '~/types'; -import { useTheme } from '~/utils/themeContext'; - -export default function ArtistDetailScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); - const artistName = decodeURIComponent(id || ''); - const { colors } = useTheme(); - const playSong = usePlayerStore((s) => s.playSong); - const [songs, setSongs] = useState([]); - - useEffect(() => { - if (artistName) { - getSongsByArtist(artistName).then(setSongs); - } - }, [artistName]); - - const albumCount = new Set(songs.map((s) => s.album).filter(Boolean)).size; - - return ( - - {/* Artist Header */} - - - - {artistName} - - - {songs.length} Songs · {albumCount} Alben - - - - playSong(song, songs, index)} - emptyTitle="Keine Songs" - /> - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/player.tsx b/apps-archived/mukke/apps/mobile/app/player.tsx deleted file mode 100644 index 06cd21c76..000000000 --- a/apps-archived/mukke/apps/mobile/app/player.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { View, Text, Pressable } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { Artwork } from '~/components/Artwork'; -import { ProgressBar } from '~/components/ProgressBar'; -import { TransportControls } from '~/components/TransportControls'; -import { useAudio } from '~/contexts/AudioContext'; -import { usePlayerStore } from '~/stores/playerStore'; -import { useLibraryStore } from '~/stores/libraryStore'; -import { useTheme } from '~/utils/themeContext'; - -export default function PlayerScreen() { - const { colors } = useTheme(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const { seekTo } = useAudio(); - const currentSong = usePlayerStore((s) => s.currentSong); - const position = usePlayerStore((s) => s.position); - const duration = usePlayerStore((s) => s.duration); - const toggleFavorite = useLibraryStore((s) => s.toggleFavorite); - - if (!currentSong) { - return ( - - Kein Song wird abgespielt - - ); - } - - return ( - - {/* Header */} - - router.back()} style={{ padding: 4 }}> - - - - WIRD ABGESPIELT - - router.push('/queue')} style={{ padding: 4 }}> - - - - - {/* Artwork */} - - - - - {/* Song Info */} - - - - - {currentSong.title} - - - {currentSong.artist || 'Unbekannt'} - - - toggleFavorite(currentSong.id)} style={{ padding: 8 }}> - - - - - - {/* Progress */} - - - {/* Transport */} - - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/playlist/[id].tsx b/apps-archived/mukke/apps/mobile/app/playlist/[id].tsx deleted file mode 100644 index f535999c4..000000000 --- a/apps-archived/mukke/apps/mobile/app/playlist/[id].tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Stack, useLocalSearchParams } from 'expo-router'; -import { useEffect, useState } from 'react'; -import { View, Text, Pressable, Alert } from 'react-native'; - -import { EmptyState } from '~/components/EmptyState'; -import { SongList } from '~/components/SongList'; -import { SongPicker } from '~/components/SongPicker'; -import { - getPlaylistById, - getPlaylistSongs, - addSongToPlaylist, - removeSongFromPlaylist, -} from '~/services/playlistService'; -import { usePlayerStore } from '~/stores/playerStore'; -import type { Playlist, Song } from '~/types'; -import { useTheme } from '~/utils/themeContext'; - -export default function PlaylistDetailScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); - const { colors } = useTheme(); - const playSong = usePlayerStore((s) => s.playSong); - const [playlist, setPlaylist] = useState(null); - const [songs, setSongs] = useState([]); - const [showPicker, setShowPicker] = useState(false); - - const loadData = async () => { - if (!id) return; - const [p, s] = await Promise.all([getPlaylistById(id), getPlaylistSongs(id)]); - setPlaylist(p); - setSongs(s); - }; - - useEffect(() => { - loadData(); - }, [id]); - - const handleAddSongs = async (selected: Song[]) => { - if (!id) return; - for (const song of selected) { - await addSongToPlaylist(id, song.id); - } - await loadData(); - }; - - const handleLongPress = (song: Song) => { - Alert.alert('Song entfernen', `"${song.title}" aus der Playlist entfernen?`, [ - { text: 'Abbrechen', style: 'cancel' }, - { - text: 'Entfernen', - style: 'destructive', - onPress: async () => { - if (id) { - await removeSongFromPlaylist(id, song.id); - await loadData(); - } - }, - }, - ]); - }; - - return ( - - ( - setShowPicker(true)} style={{ padding: 8 }}> - - - ), - }} - /> - - {playlist && ( - - - - - - {playlist.name} - - {playlist.description && ( - - {playlist.description} - - )} - - {songs.length} Songs - - - )} - - playSong(song, songs, index)} - emptyTitle="Playlist ist leer" - emptyMessage="Füge Songs über den + Button hinzu." - /> - - setShowPicker(false)} - onSelect={handleAddSongs} - excludeIds={songs.map((s) => s.id)} - /> - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/playlist/new.tsx b/apps-archived/mukke/apps/mobile/app/playlist/new.tsx deleted file mode 100644 index 7aa3279cc..000000000 --- a/apps-archived/mukke/apps/mobile/app/playlist/new.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useRouter } from 'expo-router'; -import { useState } from 'react'; -import { View, Text, TextInput, Pressable } from 'react-native'; - -import { usePlaylistStore } from '~/stores/playlistStore'; -import { useTheme } from '~/utils/themeContext'; - -export default function NewPlaylistScreen() { - const { colors } = useTheme(); - const router = useRouter(); - const createPlaylist = usePlaylistStore((s) => s.createPlaylist); - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - - const handleCreate = async () => { - if (!name.trim()) return; - const playlist = await createPlaylist(name.trim(), description.trim() || undefined); - router.dismiss(); - router.push(`/playlist/${playlist.id}`); - }; - - return ( - - Name - - - - Beschreibung (optional) - - - - - - Erstellen - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/app/queue.tsx b/apps-archived/mukke/apps/mobile/app/queue.tsx deleted file mode 100644 index 6ae972dc9..000000000 --- a/apps-archived/mukke/apps/mobile/app/queue.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { View, Text, FlatList } from 'react-native'; - -import { Artwork } from '~/components/Artwork'; -import { ListItem } from '~/components/ListItem'; -import { usePlayerStore } from '~/stores/playerStore'; -import { formatDuration } from '~/services/audioService'; -import { useTheme } from '~/utils/themeContext'; - -export default function QueueScreen() { - const { colors } = useTheme(); - const queue = usePlayerStore((s) => s.getQueue()); - const currentSong = usePlayerStore((s) => s.currentSong); - const playSong = usePlayerStore((s) => s.playSong); - - const currentIndex = queue.findIndex((s) => s.id === currentSong?.id); - - return ( - - {currentSong && ( - - - AKTUELLER SONG - - - - - - {currentSong.title} - - - {currentSong.artist || 'Unbekannt'} - - - - - )} - - - ALS NÄCHSTES - - - `${item.id}-${index}`} - contentContainerStyle={{ paddingBottom: 40 }} - renderItem={({ item, index }) => ( - } - onPress={() => playSong(item, queue, currentIndex + 1 + index)} - /> - )} - /> - - ); -} diff --git a/apps-archived/mukke/apps/mobile/assets/adaptive-icon.png b/apps-archived/mukke/apps/mobile/assets/adaptive-icon.png deleted file mode 100644 index 35c8a1c1f..000000000 Binary files a/apps-archived/mukke/apps/mobile/assets/adaptive-icon.png and /dev/null differ diff --git a/apps-archived/mukke/apps/mobile/assets/favicon.png b/apps-archived/mukke/apps/mobile/assets/favicon.png deleted file mode 100644 index 99d248c29..000000000 Binary files a/apps-archived/mukke/apps/mobile/assets/favicon.png and /dev/null differ diff --git a/apps-archived/mukke/apps/mobile/assets/icon.png b/apps-archived/mukke/apps/mobile/assets/icon.png deleted file mode 100644 index 35c8a1c1f..000000000 Binary files a/apps-archived/mukke/apps/mobile/assets/icon.png and /dev/null differ diff --git a/apps-archived/mukke/apps/mobile/assets/splash.png b/apps-archived/mukke/apps/mobile/assets/splash.png deleted file mode 100644 index eb2472ec4..000000000 Binary files a/apps-archived/mukke/apps/mobile/assets/splash.png and /dev/null differ diff --git a/apps-archived/mukke/apps/mobile/babel.config.js b/apps-archived/mukke/apps/mobile/babel.config.js deleted file mode 100644 index d9beb89e8..000000000 --- a/apps-archived/mukke/apps/mobile/babel.config.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function (api) { - api.cache(true); - const plugins = []; - - return { - presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'], - - plugins, - }; -}; diff --git a/apps-archived/mukke/apps/mobile/components/AlbumGrid.tsx b/apps-archived/mukke/apps/mobile/components/AlbumGrid.tsx deleted file mode 100644 index 16acad16c..000000000 --- a/apps-archived/mukke/apps/mobile/components/AlbumGrid.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { FlatList, Image, Pressable, Text, View, useWindowDimensions } from 'react-native'; - -import type { Album } from '~/types'; -import { useTheme } from '~/utils/themeContext'; - -import { EmptyState } from './EmptyState'; - -interface AlbumGridProps { - albums: Album[]; -} - -export function AlbumGrid({ albums }: AlbumGridProps) { - const router = useRouter(); - const { colors } = useTheme(); - const { width } = useWindowDimensions(); - const itemSize = (width - 48) / 2; - - if (albums.length === 0) { - return ( - - ); - } - - return ( - item.name} - numColumns={2} - contentContainerStyle={{ padding: 12, paddingBottom: 100 }} - columnWrapperStyle={{ gap: 12 }} - ItemSeparatorComponent={() => } - renderItem={({ item }) => ( - router.push(`/album/${encodeURIComponent(item.name)}`)} - style={{ width: itemSize }} - > - {item.coverArtPath ? ( - - ) : ( - - - - )} - - {item.name} - - - {item.artist || 'Unbekannt'} · {item.songCount} Songs - - - )} - /> - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/ArtistList.tsx b/apps-archived/mukke/apps/mobile/components/ArtistList.tsx deleted file mode 100644 index 324065014..000000000 --- a/apps-archived/mukke/apps/mobile/components/ArtistList.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useRouter } from 'expo-router'; -import { FlatList } from 'react-native'; - -import type { Artist } from '~/types'; - -import { Artwork } from './Artwork'; -import { EmptyState } from './EmptyState'; -import { ListItem } from './ListItem'; - -interface ArtistListProps { - artists: Artist[]; -} - -export function ArtistList({ artists }: ArtistListProps) { - const router = useRouter(); - - if (artists.length === 0) { - return ( - - ); - } - - return ( - item.name} - contentContainerStyle={{ paddingBottom: 100 }} - renderItem={({ item }) => ( - } - onPress={() => router.push(`/artist/${encodeURIComponent(item.name)}`)} - showChevron - /> - )} - /> - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/Artwork.tsx b/apps-archived/mukke/apps/mobile/components/Artwork.tsx deleted file mode 100644 index 28db3282a..000000000 --- a/apps-archived/mukke/apps/mobile/components/Artwork.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Image, View } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; - -interface ArtworkProps { - uri: string | null | undefined; - size?: number; - rounded?: boolean; -} - -export function Artwork({ uri, size = 48, rounded = false }: ArtworkProps) { - const { colors } = useTheme(); - - if (uri) { - return ( - - ); - } - - return ( - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/Button.tsx b/apps-archived/mukke/apps/mobile/components/Button.tsx deleted file mode 100644 index 1224076da..000000000 --- a/apps-archived/mukke/apps/mobile/components/Button.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Pressable, Text, ActivityIndicator } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; - -interface ButtonProps { - title: string; - onPress: () => void; - variant?: 'primary' | 'secondary' | 'ghost'; - loading?: boolean; - disabled?: boolean; -} - -export function Button({ title, onPress, variant = 'primary', loading, disabled }: ButtonProps) { - const { colors } = useTheme(); - - const bgColor = - variant === 'primary' - ? colors.primary - : variant === 'secondary' - ? colors.backgroundTertiary - : 'transparent'; - - const textColor = variant === 'primary' ? '#FFFFFF' : colors.text; - - return ( - - {loading ? ( - - ) : ( - {title} - )} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/EmptyState.tsx b/apps-archived/mukke/apps/mobile/components/EmptyState.tsx deleted file mode 100644 index ba03907d8..000000000 --- a/apps-archived/mukke/apps/mobile/components/EmptyState.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { View, Text } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; - -interface EmptyStateProps { - icon?: keyof typeof Ionicons.glyphMap; - title: string; - message?: string; -} - -export function EmptyState({ icon = 'musical-notes-outline', title, message }: EmptyStateProps) { - const { colors } = useTheme(); - - return ( - - - - {title} - - {message && ( - - {message} - - )} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/GenreList.tsx b/apps-archived/mukke/apps/mobile/components/GenreList.tsx deleted file mode 100644 index cfb5de52d..000000000 --- a/apps-archived/mukke/apps/mobile/components/GenreList.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { FlatList, View } from 'react-native'; - -import type { Genre } from '~/types'; -import { useTheme } from '~/utils/themeContext'; -import { usePlayerStore } from '~/stores/playerStore'; -import { getSongsByGenre } from '~/services/libraryService'; - -import { EmptyState } from './EmptyState'; -import { ListItem } from './ListItem'; - -interface GenreListProps { - genres: Genre[]; -} - -export function GenreList({ genres }: GenreListProps) { - const { colors } = useTheme(); - const playSong = usePlayerStore((s) => s.playSong); - - if (genres.length === 0) { - return ( - - ); - } - - const handlePress = async (genre: Genre) => { - const songs = await getSongsByGenre(genre.name); - if (songs.length > 0) { - playSong(songs[0], songs, 0); - } - }; - - return ( - item.name} - contentContainerStyle={{ paddingBottom: 100 }} - renderItem={({ item }) => ( - - - - } - onPress={() => handlePress(item)} - /> - )} - /> - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/ImportButton.tsx b/apps-archived/mukke/apps/mobile/components/ImportButton.tsx deleted file mode 100644 index 3202c5b9f..000000000 --- a/apps-archived/mukke/apps/mobile/components/ImportButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { useState } from 'react'; -import { Pressable, Alert } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; -import { pickAndImportFiles } from '~/services/importService'; -import { useLibraryStore } from '~/stores/libraryStore'; - -export function ImportButton() { - const { colors } = useTheme(); - const [importing, setImporting] = useState(false); - const loadAll = useLibraryStore((s) => s.loadAll); - - const handleImport = async () => { - if (importing) return; - setImporting(true); - try { - const songs = await pickAndImportFiles(); - if (songs.length > 0) { - await loadAll(); - Alert.alert('Importiert', `${songs.length} Song${songs.length > 1 ? 's' : ''} importiert.`); - } - } catch (error) { - console.error('Import failed:', error); - Alert.alert('Fehler', 'Beim Import ist ein Fehler aufgetreten.'); - } finally { - setImporting(false); - } - }; - - return ( - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/ListItem.tsx b/apps-archived/mukke/apps/mobile/components/ListItem.tsx deleted file mode 100644 index d1eabaa91..000000000 --- a/apps-archived/mukke/apps/mobile/components/ListItem.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Pressable, View, Text } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; - -interface ListItemProps { - title: string; - subtitle?: string; - trailing?: string; - left?: React.ReactNode; - onPress?: () => void; - onLongPress?: () => void; - showChevron?: boolean; -} - -export function ListItem({ - title, - subtitle, - trailing, - left, - onPress, - onLongPress, - showChevron, -}: ListItemProps) { - const { colors } = useTheme(); - - return ( - ({ - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 10, - paddingHorizontal: 16, - backgroundColor: pressed ? colors.backgroundTertiary : 'transparent', - })} - > - {left && {left}} - - - {title} - - {subtitle && ( - - {subtitle} - - )} - - {trailing && ( - {trailing} - )} - {showChevron && ( - - )} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/MiniPlayer.tsx b/apps-archived/mukke/apps/mobile/components/MiniPlayer.tsx deleted file mode 100644 index e31aa730d..000000000 --- a/apps-archived/mukke/apps/mobile/components/MiniPlayer.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { Pressable, View, Text } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; -import { useAudio } from '~/contexts/AudioContext'; -import { usePlayerStore } from '~/stores/playerStore'; - -import { Artwork } from './Artwork'; - -export function MiniPlayer() { - const { colors } = useTheme(); - const router = useRouter(); - const { play, pause, playNext } = useAudio(); - const currentSong = usePlayerStore((s) => s.currentSong); - const isPlaying = usePlayerStore((s) => s.isPlaying); - const position = usePlayerStore((s) => s.position); - const duration = usePlayerStore((s) => s.duration); - - if (!currentSong) return null; - - const progress = duration > 0 ? position / duration : 0; - - return ( - router.push('/player')} - style={{ - position: 'absolute', - bottom: 49, - left: 0, - right: 0, - backgroundColor: colors.card, - borderTopWidth: 0.5, - borderTopColor: colors.border, - }} - > - {/* Progress indicator */} - - - - - - - - - - {currentSong.title} - - - {currentSong.artist || 'Unbekannt'} - - - - - - - - - - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/ProgressBar.tsx b/apps-archived/mukke/apps/mobile/components/ProgressBar.tsx deleted file mode 100644 index ecba85bdc..000000000 --- a/apps-archived/mukke/apps/mobile/components/ProgressBar.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Slider from '@react-native-community/slider'; -import { View, Text } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; -import { formatDuration } from '~/services/audioService'; - -interface ProgressBarProps { - position: number; - duration: number; - onSeek: (position: number) => void; -} - -export function ProgressBar({ position, duration, onSeek }: ProgressBarProps) { - const { colors } = useTheme(); - - return ( - - 0 ? position / duration : 0} - onSlidingComplete={(value) => onSeek(value * duration)} - minimumValue={0} - maximumValue={1} - minimumTrackTintColor={colors.primary} - maximumTrackTintColor={colors.backgroundTertiary} - thumbTintColor={colors.primary} - /> - - - {formatDuration(position)} - - - -{formatDuration(Math.max(0, duration - position))} - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/SegmentedControl.tsx b/apps-archived/mukke/apps/mobile/components/SegmentedControl.tsx deleted file mode 100644 index 50eadf3db..000000000 --- a/apps-archived/mukke/apps/mobile/components/SegmentedControl.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Pressable, View, Text } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; - -interface SegmentedControlProps { - segments: { key: T; label: string }[]; - selected: T; - onSelect: (key: T) => void; -} - -export function SegmentedControl({ - segments, - selected, - onSelect, -}: SegmentedControlProps) { - const { colors } = useTheme(); - - return ( - - {segments.map((seg) => { - const isActive = seg.key === selected; - return ( - onSelect(seg.key)} - style={{ - flex: 1, - paddingVertical: 8, - borderRadius: 6, - backgroundColor: isActive ? colors.card : 'transparent', - alignItems: 'center', - }} - > - - {seg.label} - - - ); - })} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/SongList.tsx b/apps-archived/mukke/apps/mobile/components/SongList.tsx deleted file mode 100644 index a459853cd..000000000 --- a/apps-archived/mukke/apps/mobile/components/SongList.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FlatList } from 'react-native'; - -import type { Song } from '~/types'; -import { formatDuration } from '~/services/audioService'; -import { usePlayerStore } from '~/stores/playerStore'; - -import { Artwork } from './Artwork'; -import { EmptyState } from './EmptyState'; -import { ListItem } from './ListItem'; - -interface SongListProps { - songs: Song[]; - onSongPress?: (song: Song, index: number) => void; - emptyTitle?: string; - emptyMessage?: string; -} - -export function SongList({ - songs, - onSongPress, - emptyTitle = 'Keine Songs', - emptyMessage = 'Importiere Songs über den + Button.', -}: SongListProps) { - const playSong = usePlayerStore((s) => s.playSong); - - const handlePress = (song: Song, index: number) => { - if (onSongPress) { - onSongPress(song, index); - } else { - playSong(song, songs, index); - } - }; - - if (songs.length === 0) { - return ; - } - - return ( - item.id} - renderItem={({ item, index }) => ( - } - onPress={() => handlePress(item, index)} - /> - )} - contentContainerStyle={{ paddingBottom: 100 }} - /> - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/SongPicker.tsx b/apps-archived/mukke/apps/mobile/components/SongPicker.tsx deleted file mode 100644 index 88620afb1..000000000 --- a/apps-archived/mukke/apps/mobile/components/SongPicker.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { useState, useEffect } from 'react'; -import { FlatList, Pressable, View, Text, Modal } from 'react-native'; - -import type { Song } from '~/types'; -import { useTheme } from '~/utils/themeContext'; -import { getAllSongs } from '~/services/libraryService'; - -import { Artwork } from './Artwork'; - -interface SongPickerProps { - visible: boolean; - onClose: () => void; - onSelect: (songs: Song[]) => void; - excludeIds?: string[]; -} - -export function SongPicker({ visible, onClose, onSelect, excludeIds = [] }: SongPickerProps) { - const { colors } = useTheme(); - const [songs, setSongs] = useState([]); - const [selected, setSelected] = useState>(new Set()); - - useEffect(() => { - if (visible) { - getAllSongs().then((all) => { - setSongs(all.filter((s) => !excludeIds.includes(s.id))); - }); - setSelected(new Set()); - } - }, [visible]); - - const toggleSelection = (id: string) => { - setSelected((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; - - const handleDone = () => { - const selectedSongs = songs.filter((s) => selected.has(s.id)); - onSelect(selectedSongs); - onClose(); - }; - - return ( - - - - - Abbrechen - - - Songs auswählen - - - - Fertig ({selected.size}) - - - - - item.id} - renderItem={({ item }) => { - const isSelected = selected.has(item.id); - return ( - toggleSelection(item.id)} - style={{ - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 10, - paddingHorizontal: 16, - }} - > - - - - - {item.title} - - - {item.artist || 'Unbekannt'} - - - - ); - }} - /> - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/SortMenu.tsx b/apps-archived/mukke/apps/mobile/components/SortMenu.tsx deleted file mode 100644 index a86f97a30..000000000 --- a/apps-archived/mukke/apps/mobile/components/SortMenu.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { useState } from 'react'; -import { Pressable, View, Text, Modal } from 'react-native'; - -import type { SortField, SortDirection } from '~/types'; -import { useTheme } from '~/utils/themeContext'; - -const SORT_OPTIONS: { field: SortField; label: string }[] = [ - { field: 'title', label: 'Titel' }, - { field: 'artist', label: 'Künstler' }, - { field: 'album', label: 'Album' }, - { field: 'addedAt', label: 'Hinzugefügt' }, - { field: 'playCount', label: 'Wiedergaben' }, -]; - -interface SortMenuProps { - currentField: SortField; - currentDirection: SortDirection; - onSort: (field: SortField, direction: SortDirection) => void; -} - -export function SortMenu({ currentField, currentDirection, onSort }: SortMenuProps) { - const { colors } = useTheme(); - const [visible, setVisible] = useState(false); - - return ( - <> - setVisible(true)} style={{ padding: 8 }}> - - - - - setVisible(false)} - style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'flex-end' }} - > - - - Sortieren - - {SORT_OPTIONS.map((opt) => { - const isActive = opt.field === currentField; - return ( - { - if (isActive) { - onSort(opt.field, currentDirection === 'asc' ? 'desc' : 'asc'); - } else { - onSort(opt.field, 'asc'); - } - setVisible(false); - }} - style={{ - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 14, - borderBottomWidth: 0.5, - borderBottomColor: colors.border, - }} - > - - {opt.label} - - {isActive && ( - - )} - - ); - })} - - - - - ); -} diff --git a/apps-archived/mukke/apps/mobile/components/ThemeWrapper.tsx b/apps-archived/mukke/apps/mobile/components/ThemeWrapper.tsx deleted file mode 100644 index 7c603ad10..000000000 --- a/apps-archived/mukke/apps/mobile/components/ThemeWrapper.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { View } from 'react-native'; - -import { useTheme } from '../utils/themeContext'; - -type ThemeWrapperProps = { - children: React.ReactNode; - className?: string; -}; - -export const ThemeWrapper: React.FC = ({ children, className = '' }) => { - const { isDarkMode } = useTheme(); - - return ( - - {children} - - ); -}; diff --git a/apps-archived/mukke/apps/mobile/components/TransportControls.tsx b/apps-archived/mukke/apps/mobile/components/TransportControls.tsx deleted file mode 100644 index c74db9864..000000000 --- a/apps-archived/mukke/apps/mobile/components/TransportControls.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { View, Pressable } from 'react-native'; - -import { useTheme } from '~/utils/themeContext'; -import { useAudio } from '~/contexts/AudioContext'; -import { usePlayerStore } from '~/stores/playerStore'; -import type { RepeatMode, ShuffleMode } from '~/types'; - -interface TransportControlsProps { - size?: 'small' | 'large'; -} - -function getRepeatIcon(mode: RepeatMode): keyof typeof Ionicons.glyphMap { - if (mode === 'one') return 'repeat'; - return 'repeat'; -} - -export function TransportControls({ size = 'large' }: TransportControlsProps) { - const { colors } = useTheme(); - const { play, pause, playNext, playPrevious } = useAudio(); - const isPlaying = usePlayerStore((s) => s.isPlaying); - const repeatMode = usePlayerStore((s) => s.repeatMode); - const shuffleMode = usePlayerStore((s) => s.shuffleMode); - const toggleRepeat = usePlayerStore((s) => s.toggleRepeat); - const toggleShuffle = usePlayerStore((s) => s.toggleShuffle); - - const iconSize = size === 'large' ? 36 : 24; - const playSize = size === 'large' ? 56 : 32; - - return ( - - {size === 'large' && ( - - - - )} - - - - - - - - - - - - - - {size === 'large' && ( - - - {repeatMode === 'one' && ( - - - - )} - - )} - - ); -} diff --git a/apps-archived/mukke/apps/mobile/contexts/AudioContext.tsx b/apps-archived/mukke/apps/mobile/contexts/AudioContext.tsx deleted file mode 100644 index 542811dc1..000000000 --- a/apps-archived/mukke/apps/mobile/contexts/AudioContext.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useAudioPlayer, useAudioPlayerStatus, setAudioModeAsync } from 'expo-audio'; -import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react'; - -import { usePlayerStore } from '~/stores/playerStore'; -import { updatePlayStats, updateSongDuration } from '~/services/libraryService'; - -interface AudioContextType { - play: () => void; - pause: () => void; - seekTo: (position: number) => void; - playNext: () => void; - playPrevious: () => void; -} - -const AudioCtx = createContext({ - play: () => {}, - pause: () => {}, - seekTo: () => {}, - playNext: () => {}, - playPrevious: () => {}, -}); - -export const useAudio = () => useContext(AudioCtx); - -export const AudioProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const player = useAudioPlayer(null); - const status = useAudioPlayerStatus(player); - const { currentSong, isPlaying, setPlaying, setPosition, setDuration, nextSong, previousSong } = - usePlayerStore(); - const hasCountedPlay = useRef(false); - const lastSongId = useRef(null); - - // Configure audio mode for background playback - useEffect(() => { - setAudioModeAsync({ - playsInSilentMode: true, - shouldPlayInBackground: true, - }); - }, []); - - // Load song when currentSong changes - useEffect(() => { - if (!currentSong) return; - if (lastSongId.current === currentSong.id) return; - lastSongId.current = currentSong.id; - hasCountedPlay.current = false; - - player.replace({ uri: currentSong.filePath }); - - // Set lock screen metadata - player.setActiveForLockScreen(true, { - title: currentSong.title, - artist: currentSong.artist || undefined, - albumTitle: currentSong.album || undefined, - artworkSource: currentSong.coverArtPath ? { uri: currentSong.coverArtPath } : undefined, - }); - - player.play(); - }, [currentSong?.id]); - - // Sync play/pause state - useEffect(() => { - if (!currentSong) return; - if (isPlaying && !status.playing) { - player.play(); - } else if (!isPlaying && status.playing) { - player.pause(); - } - }, [isPlaying]); - - // Update position and duration from player status - useEffect(() => { - if (status.currentTime !== undefined) { - setPosition(status.currentTime); - } - if (status.duration && status.duration > 0) { - setDuration(status.duration); - // Save duration to DB if not yet stored - if (currentSong && !currentSong.duration) { - updateSongDuration(currentSong.id, status.duration); - } - } - }, [status.currentTime, status.duration]); - - // Count play after 10 seconds - useEffect(() => { - if (currentSong && status.currentTime > 10 && !hasCountedPlay.current) { - hasCountedPlay.current = true; - updatePlayStats(currentSong.id); - } - }, [status.currentTime]); - - // Auto-advance when track ends - useEffect(() => { - if (status.didJustFinish) { - const next = nextSong(); - if (!next) { - setPlaying(false); - } - } - }, [status.didJustFinish]); - - const play = useCallback(() => { - player.play(); - setPlaying(true); - }, [player]); - - const pause = useCallback(() => { - player.pause(); - setPlaying(false); - }, [player]); - - const seekTo = useCallback( - (position: number) => { - player.seekTo(position); - setPosition(position); - }, - [player] - ); - - const playNext = useCallback(() => { - const song = nextSong(); - if (!song) setPlaying(false); - }, []); - - const playPrevious = useCallback(() => { - const song = previousSong(); - if (song && song.id === currentSong?.id) { - // Restart current song - player.seekTo(0); - setPosition(0); - } - }, [currentSong?.id, player]); - - return ( - - {children} - - ); -}; diff --git a/apps-archived/mukke/apps/mobile/eas.json b/apps-archived/mukke/apps/mobile/eas.json deleted file mode 100644 index 4e29cc0d1..000000000 --- a/apps-archived/mukke/apps/mobile/eas.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "cli": { - "version": ">= 16.17.4", - "appVersionSource": "remote" - }, - "build": { - "base": { - "node": "22.15.0", - "pnpm": "10.18.1", - "env": { - "PNPM_WORKSPACE_ROOT": "../..", - "EAS_BUILD": "true" - }, - "cache": { - "disabled": false, - "key": "v1", - "cacheDefaultPaths": true, - "customPaths": ["node_modules", "../../node_modules"] - } - }, - "development": { - "extends": "base", - "developmentClient": true, - "distribution": "internal" - }, - "preview": { - "extends": "base", - "distribution": "internal" - }, - "production": { - "extends": "base", - "autoIncrement": true - } - }, - "submit": { - "production": {} - } -} diff --git a/apps-archived/mukke/apps/mobile/global.css b/apps-archived/mukke/apps/mobile/global.css deleted file mode 100644 index b5c61c956..000000000 --- a/apps-archived/mukke/apps/mobile/global.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/apps-archived/mukke/apps/mobile/metro.config.js b/apps-archived/mukke/apps/mobile/metro.config.js deleted file mode 100644 index 6bc2c9dd0..000000000 --- a/apps-archived/mukke/apps/mobile/metro.config.js +++ /dev/null @@ -1,25 +0,0 @@ -// Learn more https://docs.expo.io/guides/customizing-metro -const { getDefaultConfig } = require('expo/metro-config'); -const { withNativeWind } = require('nativewind/metro'); -const path = require('path'); - -// Get the project and workspace root directories -const projectRoot = __dirname; -const monorepoRoot = path.resolve(projectRoot, '../../../..'); - -/** @type {import('expo/metro-config').MetroConfig} */ -const config = getDefaultConfig(projectRoot); - -// Watch all files within the monorepo (needed for workspace packages like @mukke/types) -config.watchFolders = [path.resolve(projectRoot, '../../packages'), monorepoRoot + '/node_modules']; - -// Let Metro know where to resolve packages and in what order -config.resolver.nodeModulesPaths = [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(monorepoRoot, 'node_modules'), -]; - -// Support .cjs and .mjs extensions -config.resolver.sourceExts = [...config.resolver.sourceExts, 'cjs', 'mjs']; - -module.exports = withNativeWind(config, { input: './global.css' }); diff --git a/apps-archived/mukke/apps/mobile/nativewind-env.d.ts b/apps-archived/mukke/apps/mobile/nativewind-env.d.ts deleted file mode 100644 index 958346287..000000000 --- a/apps-archived/mukke/apps/mobile/nativewind-env.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// - -// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. diff --git a/apps-archived/mukke/apps/mobile/package.json b/apps-archived/mukke/apps/mobile/package.json deleted file mode 100644 index c520df9a8..000000000 --- a/apps-archived/mukke/apps/mobile/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "@mukke/mobile", - "version": "1.0.0", - "main": "expo-router/entry", - "scripts": { - "dev": "expo start --dev-client", - "start": "expo start --dev-client", - "ios": "expo run:ios", - "android": "expo run:android", - "build:dev": "eas build --profile development", - "build:preview": "eas build --profile preview", - "build:prod": "eas build --profile production", - "prebuild": "expo prebuild", - "lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"", - "format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write" - }, - "dependencies": { - "@expo/vector-icons": "^15.0.2", - "@missingcore/audio-metadata": "^1.3.0", - "@mukke/types": "workspace:*", - "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-community/slider": "5.1.2", - "@react-navigation/native": "^7.0.3", - "expo": "~55.0.0", - "expo-audio": "~55.0.0", - "expo-constants": "~55.0.0", - "expo-dev-client": "~55.0.0", - "expo-dev-launcher": "~55.0.0", - "expo-document-picker": "~55.0.0", - "expo-file-system": "~55.0.0", - "expo-router": "~55.0.0", - "expo-sqlite": "~55.0.0", - "expo-status-bar": "~55.0.0", - "expo-system-ui": "~55.0.0", - "nativewind": "^4.2.0", - "react": "19.2.0", - "react-dom": "19.2.0", - "react-native": "0.83.2", - "react-native-gesture-handler": "~2.30.0", - "react-native-reanimated": "~4.2.1", - "react-native-safe-area-context": "~5.6.2", - "react-native-screens": "~4.23.0", - "react-native-web": "~0.21.0", - "react-native-worklets": "~0.7.2", - "uuid": "^11.1.0", - "zustand": "^5.0.0" - }, - "devDependencies": { - "@babel/core": "^7.26.0", - "@types/react": "~19.2.14", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "^9.18.0", - "eslint-config-universe": "^14.0.0", - "prettier": "^3.5.0", - "prettier-plugin-tailwindcss": "^0.6.0", - "tailwindcss": "^3.4.0", - "typescript": "~5.9.2" - }, - "eslintConfig": { - "extends": "universe/native", - "root": true - }, - "private": true -} diff --git a/apps-archived/mukke/apps/mobile/services/audioService.ts b/apps-archived/mukke/apps/mobile/services/audioService.ts deleted file mode 100644 index a23cb05ca..000000000 --- a/apps-archived/mukke/apps/mobile/services/audioService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { setAudioModeAsync } from 'expo-audio'; - -export { useAudioPlayer, useAudioPlayerStatus } from 'expo-audio'; - -export async function configureAudioMode(): Promise { - await setAudioModeAsync({ - playsInSilentMode: true, - shouldPlayInBackground: true, - }); -} - -export function formatDuration(seconds: number | null | undefined): string { - if (!seconds || seconds <= 0) return '0:00'; - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} diff --git a/apps-archived/mukke/apps/mobile/services/database.ts b/apps-archived/mukke/apps/mobile/services/database.ts deleted file mode 100644 index 1b763ec92..000000000 --- a/apps-archived/mukke/apps/mobile/services/database.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as SQLite from 'expo-sqlite'; - -let db: SQLite.SQLiteDatabase | null = null; - -export async function getDatabase(): Promise { - if (db) return db; - db = await SQLite.openDatabaseAsync('mukke.db'); - await initializeDatabase(db); - return db; -} - -async function initializeDatabase(database: SQLite.SQLiteDatabase): Promise { - await database.execAsync(` - PRAGMA journal_mode = WAL; - PRAGMA foreign_keys = ON; - - CREATE TABLE IF NOT EXISTS songs ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - artist TEXT, - album TEXT, - albumArtist TEXT, - genre TEXT, - trackNumber INTEGER, - discNumber INTEGER, - year INTEGER, - duration REAL, - filePath TEXT NOT NULL, - fileSize INTEGER, - coverArtPath TEXT, - addedAt TEXT NOT NULL, - lastPlayedAt TEXT, - playCount INTEGER DEFAULT 0, - favorite INTEGER DEFAULT 0 - ); - - CREATE TABLE IF NOT EXISTS playlists ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - coverArtPath TEXT, - createdAt TEXT NOT NULL, - updatedAt TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS playlist_songs ( - id TEXT PRIMARY KEY, - playlistId TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, - songId TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE, - sortOrder INTEGER NOT NULL, - addedAt TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist); - CREATE INDEX IF NOT EXISTS idx_songs_album ON songs(album); - CREATE INDEX IF NOT EXISTS idx_songs_genre ON songs(genre); - CREATE INDEX IF NOT EXISTS idx_songs_favorite ON songs(favorite); - CREATE INDEX IF NOT EXISTS idx_playlist_songs_playlist ON playlist_songs(playlistId); - `); -} - -export async function closeDatabase(): Promise { - if (db) { - await db.closeAsync(); - db = null; - } -} diff --git a/apps-archived/mukke/apps/mobile/services/fileService.ts b/apps-archived/mukke/apps/mobile/services/fileService.ts deleted file mode 100644 index 715c36d3b..000000000 --- a/apps-archived/mukke/apps/mobile/services/fileService.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as FileSystem from 'expo-file-system'; -import { v4 as uuidv4 } from 'uuid'; - -const MUSIC_DIR = `${FileSystem.documentDirectory}music/`; -const ARTWORK_DIR = `${FileSystem.documentDirectory}artwork/`; - -export async function ensureDirectories(): Promise { - const musicInfo = await FileSystem.getInfoAsync(MUSIC_DIR); - if (!musicInfo.exists) { - await FileSystem.makeDirectoryAsync(MUSIC_DIR, { intermediates: true }); - } - - const artworkInfo = await FileSystem.getInfoAsync(ARTWORK_DIR); - if (!artworkInfo.exists) { - await FileSystem.makeDirectoryAsync(ARTWORK_DIR, { intermediates: true }); - } -} - -export function getFileExtension(uri: string): string { - const parts = uri.split('.'); - return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'mp3'; -} - -export async function copyToMusicDirectory( - sourceUri: string -): Promise<{ path: string; id: string }> { - await ensureDirectories(); - const id = uuidv4(); - const ext = getFileExtension(sourceUri); - const destPath = `${MUSIC_DIR}${id}.${ext}`; - await FileSystem.copyAsync({ from: sourceUri, to: destPath }); - return { path: destPath, id }; -} - -export async function saveArtwork(data: Uint8Array, songId: string): Promise { - await ensureDirectories(); - const artworkPath = `${ARTWORK_DIR}${songId}.jpg`; - const base64 = uint8ArrayToBase64(data); - await FileSystem.writeAsStringAsync(artworkPath, base64, { - encoding: FileSystem.EncodingType.Base64, - }); - return artworkPath; -} - -function uint8ArrayToBase64(bytes: Uint8Array): string { - let binary = ''; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); -} - -export async function deleteFile(path: string): Promise { - const info = await FileSystem.getInfoAsync(path); - if (info.exists) { - await FileSystem.deleteAsync(path); - } -} - -export async function getStorageInfo(): Promise<{ - musicSize: number; - artworkSize: number; - totalFiles: number; -}> { - let musicSize = 0; - let artworkSize = 0; - let totalFiles = 0; - - try { - const musicFiles = await FileSystem.readDirectoryAsync(MUSIC_DIR); - for (const file of musicFiles) { - const info = await FileSystem.getInfoAsync(`${MUSIC_DIR}${file}`); - if (info.exists && !info.isDirectory && 'size' in info) { - musicSize += info.size ?? 0; - totalFiles++; - } - } - } catch { - // Directory might not exist yet - } - - try { - const artworkFiles = await FileSystem.readDirectoryAsync(ARTWORK_DIR); - for (const file of artworkFiles) { - const info = await FileSystem.getInfoAsync(`${ARTWORK_DIR}${file}`); - if (info.exists && !info.isDirectory && 'size' in info) { - artworkSize += info.size ?? 0; - } - } - } catch { - // Directory might not exist yet - } - - return { musicSize, artworkSize, totalFiles }; -} - -export function formatFileSize(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; - return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; -} diff --git a/apps-archived/mukke/apps/mobile/services/importService.ts b/apps-archived/mukke/apps/mobile/services/importService.ts deleted file mode 100644 index ebc3f3d42..000000000 --- a/apps-archived/mukke/apps/mobile/services/importService.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { getAudioMetadata } from '@missingcore/audio-metadata'; -import * as DocumentPicker from 'expo-document-picker'; -import * as FileSystem from 'expo-file-system'; - -import type { Song } from '~/types'; - -import { copyToMusicDirectory, saveArtwork } from './fileService'; -import { insertSong } from './libraryService'; - -const SUPPORTED_TYPES = ['audio/*']; - -export async function pickAndImportFiles(): Promise { - const result = await DocumentPicker.getDocumentAsync({ - type: SUPPORTED_TYPES, - multiple: true, - copyToCacheDirectory: true, - }); - - if (result.canceled || !result.assets?.length) { - return []; - } - - const importedSongs: Song[] = []; - - for (const asset of result.assets) { - try { - const song = await importSingleFile(asset); - if (song) { - importedSongs.push(song); - } - } catch (error) { - console.warn(`Failed to import ${asset.name}:`, error); - } - } - - return importedSongs; -} - -async function importSingleFile(asset: DocumentPicker.DocumentPickerAsset): Promise { - // Copy to permanent storage - const { path: filePath, id } = await copyToMusicDirectory(asset.uri); - - // Get file size - const fileInfo = await FileSystem.getInfoAsync(filePath); - const fileSize = fileInfo.exists && 'size' in fileInfo ? (fileInfo.size ?? null) : null; - - // Extract metadata - let metadata: Awaited> | null = null; - try { - metadata = await getAudioMetadata(filePath, [ - 'title', - 'artist', - 'album', - 'albumArtist', - 'genre', - 'trackNumber', - 'year', - 'picture', - ]); - } catch (error) { - console.warn('Failed to read metadata:', error); - } - - // Save cover art if available - let coverArtPath: string | null = null; - if (metadata?.metadata?.picture) { - try { - const pictureData = metadata.metadata.picture; - if (pictureData && typeof pictureData === 'object' && 'data' in pictureData) { - coverArtPath = await saveArtwork(pictureData.data as Uint8Array, id); - } - } catch (error) { - console.warn('Failed to save cover art:', error); - } - } - - // Build title from metadata or filename - const title = (metadata?.metadata?.title as string) || asset.name.replace(/\.[^.]+$/, ''); - - const song: Song = { - id, - title, - artist: (metadata?.metadata?.artist as string) || null, - album: (metadata?.metadata?.album as string) || null, - albumArtist: (metadata?.metadata?.albumArtist as string) || null, - genre: (metadata?.metadata?.genre as string) || null, - trackNumber: metadata?.metadata?.trackNumber - ? parseInt(String(metadata.metadata.trackNumber), 10) || null - : null, - discNumber: null, - year: metadata?.metadata?.year ? parseInt(String(metadata.metadata.year), 10) || null : null, - duration: null, - filePath, - fileSize, - coverArtPath, - addedAt: new Date().toISOString(), - lastPlayedAt: null, - playCount: 0, - favorite: false, - }; - - await insertSong(song); - return song; -} diff --git a/apps-archived/mukke/apps/mobile/services/libraryService.ts b/apps-archived/mukke/apps/mobile/services/libraryService.ts deleted file mode 100644 index 0c6ac7292..000000000 --- a/apps-archived/mukke/apps/mobile/services/libraryService.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { Album, Artist, Genre, Song } from '~/types'; - -import { getDatabase } from './database'; -import { deleteFile } from './fileService'; - -export async function insertSong(song: Song): Promise { - const db = await getDatabase(); - await db.runAsync( - `INSERT INTO songs (id, title, artist, album, albumArtist, genre, trackNumber, discNumber, year, duration, filePath, fileSize, coverArtPath, addedAt, lastPlayedAt, playCount, favorite) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - song.id, - song.title, - song.artist, - song.album, - song.albumArtist, - song.genre, - song.trackNumber, - song.discNumber, - song.year, - song.duration, - song.filePath, - song.fileSize, - song.coverArtPath, - song.addedAt, - song.lastPlayedAt, - song.playCount, - song.favorite ? 1 : 0 - ); -} - -export async function getAllSongs( - orderBy: string = 'title', - direction: 'ASC' | 'DESC' = 'ASC' -): Promise { - const db = await getDatabase(); - const validColumns = ['title', 'artist', 'album', 'addedAt', 'playCount']; - const col = validColumns.includes(orderBy) ? orderBy : 'title'; - const dir = direction === 'DESC' ? 'DESC' : 'ASC'; - const rows = await db.getAllAsync( - `SELECT * FROM songs ORDER BY ${col} ${dir}` - ); - return rows.map((r) => ({ ...r, favorite: r.favorite === 1 })); -} - -export async function getSongById(id: string): Promise { - const db = await getDatabase(); - const row = await db.getFirstAsync( - 'SELECT * FROM songs WHERE id = ?', - id - ); - if (!row) return null; - return { ...row, favorite: row.favorite === 1 }; -} - -export async function deleteSong(id: string): Promise { - const db = await getDatabase(); - const song = await getSongById(id); - if (song) { - await deleteFile(song.filePath); - if (song.coverArtPath) { - await deleteFile(song.coverArtPath); - } - } - await db.runAsync('DELETE FROM songs WHERE id = ?', id); -} - -export async function toggleFavorite(id: string): Promise { - const db = await getDatabase(); - const song = await getSongById(id); - if (!song) return false; - const newFav = !song.favorite; - await db.runAsync('UPDATE songs SET favorite = ? WHERE id = ?', newFav ? 1 : 0, id); - return newFav; -} - -export async function updatePlayStats(id: string): Promise { - const db = await getDatabase(); - await db.runAsync( - 'UPDATE songs SET playCount = playCount + 1, lastPlayedAt = ? WHERE id = ?', - new Date().toISOString(), - id - ); -} - -export async function updateSongDuration(id: string, duration: number): Promise { - const db = await getDatabase(); - await db.runAsync('UPDATE songs SET duration = ? WHERE id = ?', duration, id); -} - -export async function getAlbums(): Promise { - const db = await getDatabase(); - return db.getAllAsync(` - SELECT - album AS name, - COALESCE(albumArtist, artist) AS artist, - year, - (SELECT coverArtPath FROM songs s2 WHERE s2.album = songs.album AND s2.coverArtPath IS NOT NULL LIMIT 1) AS coverArtPath, - COUNT(*) AS songCount - FROM songs - WHERE album IS NOT NULL AND album != '' - GROUP BY album - ORDER BY album ASC - `); -} - -export async function getSongsByAlbum(albumName: string): Promise { - const db = await getDatabase(); - const rows = await db.getAllAsync( - 'SELECT * FROM songs WHERE album = ? ORDER BY discNumber ASC, trackNumber ASC, title ASC', - albumName - ); - return rows.map((r) => ({ ...r, favorite: r.favorite === 1 })); -} - -export async function getArtists(): Promise { - const db = await getDatabase(); - return db.getAllAsync(` - SELECT - artist AS name, - COUNT(*) AS songCount, - COUNT(DISTINCT album) AS albumCount - FROM songs - WHERE artist IS NOT NULL AND artist != '' - GROUP BY artist - ORDER BY artist ASC - `); -} - -export async function getSongsByArtist(artistName: string): Promise { - const db = await getDatabase(); - const rows = await db.getAllAsync( - 'SELECT * FROM songs WHERE artist = ? ORDER BY album ASC, trackNumber ASC, title ASC', - artistName - ); - return rows.map((r) => ({ ...r, favorite: r.favorite === 1 })); -} - -export async function getGenres(): Promise { - const db = await getDatabase(); - return db.getAllAsync(` - SELECT - genre AS name, - COUNT(*) AS songCount - FROM songs - WHERE genre IS NOT NULL AND genre != '' - GROUP BY genre - ORDER BY genre ASC - `); -} - -export async function getSongsByGenre(genreName: string): Promise { - const db = await getDatabase(); - const rows = await db.getAllAsync( - 'SELECT * FROM songs WHERE genre = ? ORDER BY artist ASC, album ASC, trackNumber ASC', - genreName - ); - return rows.map((r) => ({ ...r, favorite: r.favorite === 1 })); -} - -export async function searchSongs(query: string): Promise { - const db = await getDatabase(); - const q = `%${query}%`; - const rows = await db.getAllAsync( - 'SELECT * FROM songs WHERE title LIKE ? OR artist LIKE ? OR album LIKE ? ORDER BY title ASC LIMIT 50', - q, - q, - q - ); - return rows.map((r) => ({ ...r, favorite: r.favorite === 1 })); -} - -export async function getSongCount(): Promise { - const db = await getDatabase(); - const row = await db.getFirstAsync<{ count: number }>('SELECT COUNT(*) as count FROM songs'); - return row?.count ?? 0; -} diff --git a/apps-archived/mukke/apps/mobile/services/playlistService.ts b/apps-archived/mukke/apps/mobile/services/playlistService.ts deleted file mode 100644 index f6ae210eb..000000000 --- a/apps-archived/mukke/apps/mobile/services/playlistService.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -import type { Playlist, PlaylistSong, Song } from '~/types'; - -import { getDatabase } from './database'; - -export async function createPlaylist(name: string, description?: string): Promise { - const db = await getDatabase(); - const now = new Date().toISOString(); - const playlist: Playlist = { - id: uuidv4(), - name, - description: description || null, - coverArtPath: null, - createdAt: now, - updatedAt: now, - }; - await db.runAsync( - 'INSERT INTO playlists (id, name, description, coverArtPath, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)', - playlist.id, - playlist.name, - playlist.description, - playlist.coverArtPath, - playlist.createdAt, - playlist.updatedAt - ); - return playlist; -} - -export async function getAllPlaylists(): Promise { - const db = await getDatabase(); - return db.getAllAsync('SELECT * FROM playlists ORDER BY updatedAt DESC'); -} - -export async function getPlaylistById(id: string): Promise { - const db = await getDatabase(); - return db.getFirstAsync('SELECT * FROM playlists WHERE id = ?', id); -} - -export async function updatePlaylist( - id: string, - updates: { name?: string; description?: string } -): Promise { - const db = await getDatabase(); - const sets: string[] = ['updatedAt = ?']; - const values: (string | null)[] = [new Date().toISOString()]; - - if (updates.name !== undefined) { - sets.push('name = ?'); - values.push(updates.name); - } - if (updates.description !== undefined) { - sets.push('description = ?'); - values.push(updates.description); - } - - values.push(id); - await db.runAsync(`UPDATE playlists SET ${sets.join(', ')} WHERE id = ?`, ...values); -} - -export async function deletePlaylist(id: string): Promise { - const db = await getDatabase(); - await db.runAsync('DELETE FROM playlists WHERE id = ?', id); -} - -export async function addSongToPlaylist(playlistId: string, songId: string): Promise { - const db = await getDatabase(); - const maxOrder = await db.getFirstAsync<{ maxOrder: number | null }>( - 'SELECT MAX(sortOrder) as maxOrder FROM playlist_songs WHERE playlistId = ?', - playlistId - ); - const sortOrder = (maxOrder?.maxOrder ?? -1) + 1; - - await db.runAsync( - 'INSERT INTO playlist_songs (id, playlistId, songId, sortOrder, addedAt) VALUES (?, ?, ?, ?, ?)', - uuidv4(), - playlistId, - songId, - sortOrder, - new Date().toISOString() - ); - - await db.runAsync( - 'UPDATE playlists SET updatedAt = ? WHERE id = ?', - new Date().toISOString(), - playlistId - ); -} - -export async function removeSongFromPlaylist(playlistId: string, songId: string): Promise { - const db = await getDatabase(); - await db.runAsync( - 'DELETE FROM playlist_songs WHERE playlistId = ? AND songId = ?', - playlistId, - songId - ); - await db.runAsync( - 'UPDATE playlists SET updatedAt = ? WHERE id = ?', - new Date().toISOString(), - playlistId - ); -} - -export async function getPlaylistSongs(playlistId: string): Promise { - const db = await getDatabase(); - const rows = await db.getAllAsync( - `SELECT s.* FROM songs s - INNER JOIN playlist_songs ps ON s.id = ps.songId - WHERE ps.playlistId = ? - ORDER BY ps.sortOrder ASC`, - playlistId - ); - return rows.map((r) => ({ ...r, favorite: r.favorite === 1 })); -} - -export async function getPlaylistSongCount(playlistId: string): Promise { - const db = await getDatabase(); - const row = await db.getFirstAsync<{ count: number }>( - 'SELECT COUNT(*) as count FROM playlist_songs WHERE playlistId = ?', - playlistId - ); - return row?.count ?? 0; -} diff --git a/apps-archived/mukke/apps/mobile/services/queueService.ts b/apps-archived/mukke/apps/mobile/services/queueService.ts deleted file mode 100644 index 629c24922..000000000 --- a/apps-archived/mukke/apps/mobile/services/queueService.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Song } from '~/types'; - -export interface QueueState { - queue: Song[]; - originalQueue: Song[]; - currentIndex: number; -} - -export function createQueue(songs: Song[], startIndex: number = 0): QueueState { - return { - queue: [...songs], - originalQueue: [...songs], - currentIndex: startIndex, - }; -} - -export function shuffleQueue(state: QueueState): QueueState { - const currentSong = state.queue[state.currentIndex]; - const remaining = state.queue.filter((_, i) => i !== state.currentIndex); - - // Fisher-Yates shuffle - for (let i = remaining.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [remaining[i], remaining[j]] = [remaining[j], remaining[i]]; - } - - return { - ...state, - queue: currentSong ? [currentSong, ...remaining] : remaining, - currentIndex: 0, - }; -} - -export function unshuffleQueue(state: QueueState): QueueState { - const currentSong = state.queue[state.currentIndex]; - const newIndex = currentSong ? state.originalQueue.findIndex((s) => s.id === currentSong.id) : 0; - - return { - ...state, - queue: [...state.originalQueue], - currentIndex: Math.max(0, newIndex), - }; -} - -export function getNextIndex(state: QueueState, repeatMode: 'off' | 'all' | 'one'): number | null { - if (repeatMode === 'one') return state.currentIndex; - if (state.currentIndex < state.queue.length - 1) return state.currentIndex + 1; - if (repeatMode === 'all') return 0; - return null; -} - -export function getPreviousIndex( - state: QueueState, - repeatMode: 'off' | 'all' | 'one' -): number | null { - if (repeatMode === 'one') return state.currentIndex; - if (state.currentIndex > 0) return state.currentIndex - 1; - if (repeatMode === 'all') return state.queue.length - 1; - return null; -} diff --git a/apps-archived/mukke/apps/mobile/stores/libraryStore.ts b/apps-archived/mukke/apps/mobile/stores/libraryStore.ts deleted file mode 100644 index adc8e1e64..000000000 --- a/apps-archived/mukke/apps/mobile/stores/libraryStore.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { create } from 'zustand'; - -import type { Album, Artist, Genre, LibraryTab, Song, SortDirection, SortField } from '~/types'; -import * as libraryService from '~/services/libraryService'; - -interface LibraryState { - songs: Song[]; - albums: Album[]; - artists: Artist[]; - genres: Genre[]; - activeTab: LibraryTab; - sortField: SortField; - sortDirection: SortDirection; - isLoading: boolean; - songCount: number; - - setActiveTab: (tab: LibraryTab) => void; - setSortField: (field: SortField) => void; - setSortDirection: (dir: SortDirection) => void; - loadSongs: () => Promise; - loadAlbums: () => Promise; - loadArtists: () => Promise; - loadGenres: () => Promise; - loadAll: () => Promise; - toggleFavorite: (id: string) => Promise; -} - -export const useLibraryStore = create((set, get) => ({ - songs: [], - albums: [], - artists: [], - genres: [], - activeTab: 'songs', - sortField: 'title', - sortDirection: 'asc', - isLoading: false, - songCount: 0, - - setActiveTab: (tab) => set({ activeTab: tab }), - - setSortField: (field) => { - set({ sortField: field }); - get().loadSongs(); - }, - - setSortDirection: (dir) => { - set({ sortDirection: dir }); - get().loadSongs(); - }, - - loadSongs: async () => { - const { sortField, sortDirection } = get(); - const songs = await libraryService.getAllSongs( - sortField, - sortDirection.toUpperCase() as 'ASC' | 'DESC' - ); - set({ songs, songCount: songs.length }); - }, - - loadAlbums: async () => { - const albums = await libraryService.getAlbums(); - set({ albums }); - }, - - loadArtists: async () => { - const artists = await libraryService.getArtists(); - set({ artists }); - }, - - loadGenres: async () => { - const genres = await libraryService.getGenres(); - set({ genres }); - }, - - loadAll: async () => { - set({ isLoading: true }); - try { - await Promise.all([ - get().loadSongs(), - get().loadAlbums(), - get().loadArtists(), - get().loadGenres(), - ]); - } finally { - set({ isLoading: false }); - } - }, - - toggleFavorite: async (id) => { - const newFav = await libraryService.toggleFavorite(id); - set((state) => ({ - songs: state.songs.map((s) => (s.id === id ? { ...s, favorite: newFav } : s)), - })); - }, -})); diff --git a/apps-archived/mukke/apps/mobile/stores/playerStore.ts b/apps-archived/mukke/apps/mobile/stores/playerStore.ts deleted file mode 100644 index 3d8de9237..000000000 --- a/apps-archived/mukke/apps/mobile/stores/playerStore.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { create } from 'zustand'; - -import type { RepeatMode, ShuffleMode, Song } from '~/types'; -import { - createQueue, - getNextIndex, - getPreviousIndex, - shuffleQueue, - unshuffleQueue, - type QueueState, -} from '~/services/queueService'; - -interface PlayerState { - currentSong: Song | null; - isPlaying: boolean; - position: number; - duration: number; - repeatMode: RepeatMode; - shuffleMode: ShuffleMode; - queueState: QueueState; - - playSong: (song: Song, queue?: Song[], startIndex?: number) => void; - setPlaying: (playing: boolean) => void; - setPosition: (position: number) => void; - setDuration: (duration: number) => void; - toggleRepeat: () => void; - toggleShuffle: () => void; - nextSong: () => Song | null; - previousSong: () => Song | null; - getQueue: () => Song[]; - clearQueue: () => void; -} - -export const usePlayerStore = create((set, get) => ({ - currentSong: null, - isPlaying: false, - position: 0, - duration: 0, - repeatMode: 'off', - shuffleMode: 'off', - queueState: { queue: [], originalQueue: [], currentIndex: 0 }, - - playSong: (song, queue, startIndex) => { - let queueState: QueueState; - if (queue && queue.length > 0) { - const idx = startIndex ?? queue.findIndex((s) => s.id === song.id); - queueState = createQueue(queue, Math.max(0, idx)); - if (get().shuffleMode === 'on') { - queueState = shuffleQueue(queueState); - } - } else { - queueState = createQueue([song], 0); - } - set({ currentSong: song, isPlaying: true, position: 0, duration: 0, queueState }); - }, - - setPlaying: (playing) => set({ isPlaying: playing }), - setPosition: (position) => set({ position }), - setDuration: (duration) => set({ duration }), - - toggleRepeat: () => { - const modes: RepeatMode[] = ['off', 'all', 'one']; - const current = modes.indexOf(get().repeatMode); - set({ repeatMode: modes[(current + 1) % modes.length] }); - }, - - toggleShuffle: () => { - const { shuffleMode, queueState } = get(); - if (shuffleMode === 'off') { - set({ - shuffleMode: 'on', - queueState: shuffleQueue(queueState), - }); - } else { - set({ - shuffleMode: 'off', - queueState: unshuffleQueue(queueState), - }); - } - }, - - nextSong: () => { - const { queueState, repeatMode } = get(); - const nextIdx = getNextIndex(queueState, repeatMode); - if (nextIdx === null) { - set({ isPlaying: false }); - return null; - } - const song = queueState.queue[nextIdx]; - set({ - currentSong: song, - position: 0, - duration: 0, - isPlaying: true, - queueState: { ...queueState, currentIndex: nextIdx }, - }); - return song; - }, - - previousSong: () => { - const { queueState, repeatMode, position } = get(); - // If more than 3 seconds in, restart current song - if (position > 3) { - set({ position: 0 }); - return get().currentSong; - } - const prevIdx = getPreviousIndex(queueState, repeatMode); - if (prevIdx === null) { - set({ position: 0 }); - return get().currentSong; - } - const song = queueState.queue[prevIdx]; - set({ - currentSong: song, - position: 0, - duration: 0, - isPlaying: true, - queueState: { ...queueState, currentIndex: prevIdx }, - }); - return song; - }, - - getQueue: () => get().queueState.queue, - - clearQueue: () => { - set({ - currentSong: null, - isPlaying: false, - position: 0, - duration: 0, - queueState: { queue: [], originalQueue: [], currentIndex: 0 }, - }); - }, -})); diff --git a/apps-archived/mukke/apps/mobile/stores/playlistStore.ts b/apps-archived/mukke/apps/mobile/stores/playlistStore.ts deleted file mode 100644 index c0020bab6..000000000 --- a/apps-archived/mukke/apps/mobile/stores/playlistStore.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { create } from 'zustand'; - -import type { Playlist } from '~/types'; -import * as playlistService from '~/services/playlistService'; - -interface PlaylistState { - playlists: Playlist[]; - isLoading: boolean; - - loadPlaylists: () => Promise; - createPlaylist: (name: string, description?: string) => Promise; - deletePlaylist: (id: string) => Promise; - updatePlaylist: (id: string, updates: { name?: string; description?: string }) => Promise; -} - -export const usePlaylistStore = create((set, get) => ({ - playlists: [], - isLoading: false, - - loadPlaylists: async () => { - set({ isLoading: true }); - try { - const playlists = await playlistService.getAllPlaylists(); - set({ playlists }); - } finally { - set({ isLoading: false }); - } - }, - - createPlaylist: async (name, description) => { - const playlist = await playlistService.createPlaylist(name, description); - set((state) => ({ playlists: [playlist, ...state.playlists] })); - return playlist; - }, - - deletePlaylist: async (id) => { - await playlistService.deletePlaylist(id); - set((state) => ({ playlists: state.playlists.filter((p) => p.id !== id) })); - }, - - updatePlaylist: async (id, updates) => { - await playlistService.updatePlaylist(id, updates); - await get().loadPlaylists(); - }, -})); diff --git a/apps-archived/mukke/apps/mobile/tailwind.config.js b/apps-archived/mukke/apps/mobile/tailwind.config.js deleted file mode 100644 index 7c9a9f5e9..000000000 --- a/apps-archived/mukke/apps/mobile/tailwind.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./app/**/*.{js,ts,tsx}', './components/**/*.{js,ts,tsx}'], - darkMode: 'class', - presets: [require('nativewind/preset')], - theme: { - extend: { - colors: { - primary: '#FF6B35', - 'primary-dark': '#E55A2B', - accent: '#FF8F65', - background: { - light: '#FFFFFF', - dark: '#121212', - }, - text: { - light: '#000000', - dark: '#FFFFFF', - }, - }, - }, - }, - plugins: [], -}; diff --git a/apps-archived/mukke/apps/mobile/tsconfig.json b/apps-archived/mukke/apps/mobile/tsconfig.json deleted file mode 100644 index de988058c..000000000 --- a/apps-archived/mukke/apps/mobile/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": { - "strict": true, - "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "~/*": ["*"] - } - }, - "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"] -} diff --git a/apps-archived/mukke/apps/mobile/types/index.ts b/apps-archived/mukke/apps/mobile/types/index.ts deleted file mode 100644 index f2efaad14..000000000 --- a/apps-archived/mukke/apps/mobile/types/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type { - Song, - Album, - Artist, - Genre, - Playlist, - PlaylistSong, - RepeatMode, - ShuffleMode, - LibraryTab, - SortField, - SortDirection, - SortOption, -} from '@mukke/types'; diff --git a/apps-archived/mukke/apps/mobile/utils/themeContext.tsx b/apps-archived/mukke/apps/mobile/utils/themeContext.tsx deleted file mode 100644 index 84cb39a3a..000000000 --- a/apps-archived/mukke/apps/mobile/utils/themeContext.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { useColorScheme } from 'react-native'; - -export type ThemeVariant = 'classic' | 'ocean' | 'sunset'; -export type ThemeMode = 'light' | 'dark'; - -export interface ThemeColors { - primary: string; - primaryDark: string; - accent: string; - background: string; - backgroundSecondary: string; - backgroundTertiary: string; - text: string; - textSecondary: string; - textTertiary: string; - border: string; - card: string; - success: string; - warning: string; - error: string; - shadow: string; -} - -const THEME_VARIANTS = { - classic: { - primary: '#FF6B35', - primaryDark: '#E55A2B', - accent: '#FF8F65', - }, - ocean: { - primary: '#2196F3', - primaryDark: '#1976D2', - accent: '#42A5F5', - }, - sunset: { - primary: '#FF6B6B', - primaryDark: '#FF5252', - accent: '#FF8A80', - }, -}; - -const getThemeColors = (variant: ThemeVariant, isDark: boolean): ThemeColors => { - const variantColors = THEME_VARIANTS[variant]; - - if (isDark) { - return { - primary: variantColors.primary, - primaryDark: variantColors.primaryDark, - accent: variantColors.accent, - background: '#121212', - backgroundSecondary: '#1E1E1E', - backgroundTertiary: '#2D2D2D', - text: '#FFFFFF', - textSecondary: '#AAAAAA', - textTertiary: '#888888', - border: '#333333', - card: '#1E1E1E', - success: '#4CAF50', - warning: '#FF9800', - error: '#FF6B6B', - shadow: '#000000', - }; - } else { - return { - primary: variantColors.primary, - primaryDark: variantColors.primaryDark, - accent: variantColors.accent, - background: '#F5F5F5', - backgroundSecondary: '#FFFFFF', - backgroundTertiary: '#F0F0F0', - text: '#000000', - textSecondary: '#666666', - textTertiary: '#999999', - border: '#E0E0E0', - card: '#FFFFFF', - success: '#4CAF50', - warning: '#FF9800', - error: '#F44336', - shadow: '#000000', - }; - } -}; - -type ThemeContextType = { - isDarkMode: boolean; - themeVariant: ThemeVariant; - colors: ThemeColors; - toggleTheme: () => void; - setDarkMode: (isDark: boolean) => void; - setThemeVariant: (variant: ThemeVariant) => void; -}; - -const ThemeContext = createContext({ - isDarkMode: false, - themeVariant: 'classic', - colors: getThemeColors('classic', false), - toggleTheme: () => {}, - setDarkMode: () => {}, - setThemeVariant: () => {}, -}); - -export const useTheme = () => useContext(ThemeContext); - -const THEME_PREFERENCE_KEY = '@mukke_theme_preference'; -const THEME_VARIANT_KEY = '@mukke_theme_variant'; - -type ThemeProviderProps = { - children: React.ReactNode | ((themeProps: ThemeContextType) => React.ReactNode); -}; - -export const ThemeProvider: React.FC = ({ children }) => { - const systemColorScheme = useColorScheme(); - const [isDarkMode, setIsDarkMode] = useState(false); - const [themeVariant, setThemeVariantState] = useState('classic'); - const [isLoaded, setIsLoaded] = useState(false); - - useEffect(() => { - const loadThemePreferences = async () => { - try { - const [savedPreference, savedVariant] = await Promise.all([ - AsyncStorage.getItem(THEME_PREFERENCE_KEY), - AsyncStorage.getItem(THEME_VARIANT_KEY), - ]); - - if (savedPreference !== null) { - setIsDarkMode(savedPreference === 'dark'); - } else { - setIsDarkMode(systemColorScheme === 'dark'); - } - - if (savedVariant !== null && ['classic', 'ocean', 'sunset'].includes(savedVariant)) { - setThemeVariantState(savedVariant as ThemeVariant); - } - } catch (error) { - console.error('Failed to load theme preferences', error); - } finally { - setIsLoaded(true); - } - }; - - loadThemePreferences(); - }, [systemColorScheme]); - - useEffect(() => { - if (isLoaded) { - AsyncStorage.setItem(THEME_PREFERENCE_KEY, isDarkMode ? 'dark' : 'light').catch((error) => - console.error('Failed to save theme preference', error) - ); - } - }, [isDarkMode, isLoaded]); - - useEffect(() => { - if (isLoaded) { - AsyncStorage.setItem(THEME_VARIANT_KEY, themeVariant).catch((error) => - console.error('Failed to save theme variant', error) - ); - } - }, [themeVariant, isLoaded]); - - const toggleTheme = () => { - setIsDarkMode((prev) => !prev); - }; - - const setDarkMode = (isDark: boolean) => { - setIsDarkMode(isDark); - }; - - const setThemeVariant = (variant: ThemeVariant) => { - setThemeVariantState(variant); - }; - - const colors = getThemeColors(themeVariant, isDarkMode); - - const themeContextValue = { - isDarkMode, - themeVariant, - colors, - toggleTheme, - setDarkMode, - setThemeVariant, - }; - - return ( - - {typeof children === 'function' ? children(themeContextValue) : children} - - ); -}; diff --git a/apps-archived/mukke/package.json b/apps-archived/mukke/package.json deleted file mode 100644 index 44052b0ab..000000000 --- a/apps-archived/mukke/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "mukke", - "version": "1.0.0", - "private": true, - "description": "Mukke - Offline-first iOS Music Player", - "scripts": { - "dev": "pnpm run --filter=@mukke/* --parallel dev" - } -} diff --git a/apps-archived/mukke/packages/mukke-types/package.json b/apps-archived/mukke/packages/mukke-types/package.json deleted file mode 100644 index 36bd61b35..000000000 --- a/apps-archived/mukke/packages/mukke-types/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@mukke/types", - "version": "1.0.0", - "main": "src/index.ts", - "types": "src/index.ts", - "private": true -} diff --git a/apps-archived/mukke/packages/mukke-types/src/index.ts b/apps-archived/mukke/packages/mukke-types/src/index.ts deleted file mode 100644 index ea55fd397..000000000 --- a/apps-archived/mukke/packages/mukke-types/src/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -export interface Song { - id: string; - title: string; - artist: string | null; - album: string | null; - albumArtist: string | null; - genre: string | null; - trackNumber: number | null; - discNumber: number | null; - year: number | null; - duration: number | null; - filePath: string; - fileSize: number | null; - coverArtPath: string | null; - addedAt: string; - lastPlayedAt: string | null; - playCount: number; - favorite: boolean; -} - -export interface Album { - name: string; - artist: string | null; - year: number | null; - coverArtPath: string | null; - songCount: number; -} - -export interface Artist { - name: string; - songCount: number; - albumCount: number; -} - -export interface Genre { - name: string; - songCount: number; -} - -export interface Playlist { - id: string; - name: string; - description: string | null; - coverArtPath: string | null; - createdAt: string; - updatedAt: string; -} - -export interface PlaylistSong { - id: string; - playlistId: string; - songId: string; - sortOrder: number; - addedAt: string; -} - -export type RepeatMode = 'off' | 'all' | 'one'; -export type ShuffleMode = 'off' | 'on'; - -export type LibraryTab = 'songs' | 'albums' | 'artists' | 'genres'; - -export type SortField = 'title' | 'artist' | 'album' | 'addedAt' | 'playCount'; -export type SortDirection = 'asc' | 'desc'; - -export interface SortOption { - field: SortField; - direction: SortDirection; - label: string; -} diff --git a/apps-archived/mukke/packages/mukke-types/tsconfig.json b/apps-archived/mukke/packages/mukke-types/tsconfig.json deleted file mode 100644 index 64a30e04e..000000000 --- a/apps-archived/mukke/packages/mukke-types/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "declaration": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"] -}