chore: Refactor UI components and improve styling across various screens

This commit is contained in:
2025-12-27 04:33:25 +01:00
parent 63e30d3c4d
commit 295654f5cf
24 changed files with 640 additions and 303 deletions
+22 -12
View File
@@ -2,15 +2,13 @@ import ThemedBackground from "@/src/components/themed/ThemedBackground";
import {useAuth} from "@/src/stores/authStore";
import ThemedHeader from "@/src/components/themed/ThemedHeader";
import React, {useEffect, useState} from "react";
import Checkbox from 'expo-checkbox';
import {View} from "react-native";
import {useColors} from "@/src/hooks/useColors";
import ThemedCheckbox from "@/src/components/themed/ThemedCheckbox";
import ThemedParagraph from "@/src/components/themed/ThemedParagraph";
export default function HomeScreen() {
const [idle, setIdle] = useState(false);
const {authenticatedUser} = useAuth();
const {colors} = useColors();
useEffect(() => {
if (authenticatedUser) {
@@ -20,15 +18,27 @@ export default function HomeScreen() {
return (
<ThemedBackground>
<ThemedHeader>Willkommen!</ThemedHeader>
<View className="flex-row items-center">
<Checkbox
className="m-2"
value={idle}
onValueChange={setIdle}
color={idle ? colors.secondary : undefined}
/>
<ThemedParagraph>Energiesparmodus</ThemedParagraph>
<View className="flex-1">
<ThemedHeader
subtitle="Steuere deine LED Matrix"
>
Willkommen{authenticatedUser?.name ? `, ${authenticatedUser.name}` : ''}!
</ThemedHeader>
<View className="mt-6 gap-4">
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<ThemedParagraph className="text-left mb-3 font-semibold text-lg">
Schnelleinstellungen
</ThemedParagraph>
<ThemedCheckbox
label="Energiesparmodus"
description="Reduziert Helligkeit und deaktiviert Animationen"
value={idle}
onValueChange={setIdle}
/>
</View>
</View>
</View>
</ThemedBackground>
);
+41 -3
View File
@@ -1,12 +1,50 @@
import React from "react";
import {View, Text} from "react-native";
import {Feather} from "@expo/vector-icons";
import ThemedHeader from "@/src/components/themed/ThemedHeader";
import ThemedBackground from "@/src/components/themed/ThemedBackground";
import ColorSelector from "@/src/components/themed/ColorSelector";
import SaveToMatrixButton from "@/src/components/SaveToMatrixButton";
import { useMatrixStore } from "@/src/stores";
import {useColors} from "@/src/hooks/useColors";
export default function ClockScreen() {
const {colors} = useColors();
const clockConfig = useMatrixStore((s) => s.matrixState.clock);
const updateClockConfig = useMatrixStore((s) => s.updateClockConfig);
return (
<ThemedBackground>
<ThemedHeader>
Clock Mode
</ThemedHeader>
<View className="flex-1 justify-between">
<View>
<ThemedHeader subtitle="Zeige die Uhrzeit an">
Uhr Modus
</ThemedHeader>
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-6 mt-4">
<View className="items-center mb-6">
<View className="w-16 h-16 rounded-full bg-primary/10 dark:bg-primary-light/10 items-center justify-center mb-3">
<Feather name="clock" size={32} color={colors.primary} />
</View>
<Text className="text-base font-medium text-onSurface dark:text-onSurface-dark">
Uhr Anzeige
</Text>
</View>
<View>
<Text className="text-sm font-medium text-muted dark:text-muted-dark mb-2">
Uhrzeitfarbe
</Text>
<ColorSelector
onSelect={(color) => updateClockConfig({ color })}
defaultColor={clockConfig.color}
/>
</View>
</View>
</View>
<SaveToMatrixButton mode="clock" />
</View>
</ThemedBackground>
);
}
+61 -39
View File
@@ -17,6 +17,8 @@ import ThemedButton from "@/src/components/themed/ThemedButton";
import {useColors} from "@/src/hooks/useColors";
import {useAuth} from "@/src/stores/authStore";
import {MaterialIcons} from '@expo/vector-icons';
import { useMatrixStore } from "@/src/stores";
import SaveToMatrixButton from "@/src/components/SaveToMatrixButton";
export default function ImageScreen() {
const {token} = useAuth();
@@ -27,6 +29,9 @@ export default function ImageScreen() {
const [deletingFile, setDeletingFile] = useState<string | null>(null);
const {colors} = useColors();
const imageConfig = useMatrixStore((s) => s.matrixState.image);
const updateImageConfig = useMatrixStore((s) => s.updateImageConfig);
const fetchStoredFiles = async () => {
setLoadingFiles(true);
try {
@@ -154,98 +159,105 @@ export default function ImageScreen() {
return (
<ThemedBackground>
<ThemedHeader>
Bildschirm für Bildauswahl
<ThemedHeader subtitle="Lade Bilder hoch und zeige sie an">
Bilder Modus
</ThemedHeader>
<View className="flex-1 w-full p-4">
<View className="flex-1 w-full">
{uploading ? (
<View className="flex-1 justify-center items-center">
<View className="flex-1 justify-center items-center bg-surface dark:bg-surface-dark rounded-2xl p-8">
<ActivityIndicator size="large" color={colors.primary}/>
<Text className="mt-2.5 text-base text-onSurface dark:text-onSurface-dark">
<Text className="mt-4 text-base font-medium text-onSurface dark:text-onSurface-dark">
Datei wird hochgeladen...
</Text>
</View>
) : (
<CustomImagePicker
onSuccess={onSuccess}
onFailure={onFailure}
onCanceled={onCanceled}
/>
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<CustomImagePicker
onSuccess={onSuccess}
onFailure={onFailure}
onCanceled={onCanceled}
/>
</View>
)}
<View className="my-5 items-center">
<View className="my-4">
<ThemedButton
onPress={toggleFilesList}
title={showFiles ? "Dateien ausblenden" : "Gespeicherte Dateien anzeigen"}
mode="contained"
mode={showFiles ? "outlined" : "contained"}
icon={showFiles ? "eye-off" : "folder"}
/>
</View>
{showFiles && (
<View className="flex-1 w-full">
<ThemedHeader>Gespeicherte Dateien</ThemedHeader>
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-3">
Gespeicherte Dateien
</Text>
{loadingFiles ? (
<ActivityIndicator size="large" color={colors.primary}/>
<View className="items-center py-8">
<ActivityIndicator size="large" color={colors.primary}/>
</View>
) : files.length > 0 ? (
<FlatList
data={files}
keyExtractor={(item) => item.key}
renderItem={({item}) => (
<View
className="p-3 my-2 border rounded-lg relative border-outline dark:border-outline-dark"
>
<Text className="font-bold text-onSurface dark:text-onSurface-dark">
<View className="p-4 my-1.5 rounded-xl relative bg-surface dark:bg-surface-dark border border-outline/30 dark:border-outline-dark/30">
<Text className="font-semibold text-onSurface dark:text-onSurface-dark mb-1">
{item.originalName}
</Text>
<Text className="text-onSurface dark:text-onSurface-dark">
Typ: {item.mimeType}
<Text className="text-sm text-muted dark:text-muted-dark">
{item.mimeType} {formatFileSize(item.size)}
</Text>
<Text className="text-onSurface dark:text-onSurface-dark">
Größe: {formatFileSize(item.size)}
<Text className="text-xs text-muted dark:text-muted-dark mt-1">
{formatDate(item.lastModified)}
</Text>
<Text className="text-onSurface dark:text-onSurface-dark">
Zuletzt geändert: {formatDate(item.lastModified)}
</Text>
<View className="flex-row absolute top-3 right-3">
<View className="flex-row absolute top-3 right-3 gap-2">
<TouchableOpacity
className="w-9 h-9 rounded-full justify-center items-center ml-2 bg-primary"
className="w-10 h-10 rounded-xl justify-center items-center bg-primary"
onPress={() => viewFile(item.key)}
>
<MaterialIcons name="visibility" size={24} color="white"/>
<MaterialIcons name="visibility" size={22} color="white"/>
</TouchableOpacity>
<TouchableOpacity
className="w-9 h-9 rounded-full justify-center items-center ml-2 bg-error"
className="w-10 h-10 rounded-xl justify-center items-center bg-error"
onPress={() => confirmDeleteFile(item.key)}
>
<MaterialIcons name="delete" size={24} color="white"/>
<MaterialIcons name="delete" size={22} color="white"/>
</TouchableOpacity>
</View>
</View>
)}
/>
) : (
<Text className="text-center text-onSurface dark:text-onSurface-dark">
Keine Dateien gefunden
</Text>
<View className="items-center py-8 bg-surface dark:bg-surface-dark rounded-2xl">
<Text className="text-muted dark:text-muted-dark">
Keine Dateien gefunden
</Text>
</View>
)}
</View>
)}
<Modal
animationType="slide"
animationType="fade"
transparent={true}
visible={!!deletingFile}
onRequestClose={cancelDelete}
>
<View className="flex-1 justify-center items-center bg-black/50">
<View className="w-4/5 bg-surface dark:bg-surface-dark rounded-lg p-5 items-center">
<Text className="mb-4 text-center text-onSurface dark:text-onSurface-dark">
Sind Sie sicher, dass Sie diese Datei löschen möchten?
<View className="flex-1 justify-center items-center bg-black/60 px-6">
<View className="w-full max-w-sm bg-surface dark:bg-surface-dark rounded-2xl p-6">
<Text className="text-lg font-semibold text-center text-onSurface dark:text-onSurface-dark mb-2">
Datei löschen?
</Text>
<View className="flex-row justify-between w-full">
<Text className="text-sm text-center text-muted dark:text-muted-dark mb-6">
Diese Aktion kann nicht rückgängig gemacht werden.
</Text>
<View className="gap-2">
<ThemedButton
onPress={() => {
if (deletingFile) {
@@ -255,6 +267,7 @@ export default function ImageScreen() {
}}
title="Ja, löschen"
mode="contained"
className="bg-error"
/>
<ThemedButton
onPress={cancelDelete}
@@ -265,6 +278,15 @@ export default function ImageScreen() {
</View>
</View>
</Modal>
{imageConfig.image && (
<View className="mt-4">
<Text className="text-sm text-muted dark:text-muted-dark mb-2">
Ausgewähltes Bild: {imageConfig.image}
</Text>
<SaveToMatrixButton mode="image" />
</View>
)}
</View>
</ThemedBackground>
);
+46 -3
View File
@@ -1,13 +1,56 @@
import React from "react";
import {View, Text} from "react-native";
import {Feather} from "@expo/vector-icons";
import ThemedBackground from "@/src/components/themed/ThemedBackground";
import ThemedHeader from "@/src/components/themed/ThemedHeader";
import ThemedCheckbox from "@/src/components/themed/ThemedCheckbox";
import SaveToMatrixButton from "@/src/components/SaveToMatrixButton";
import { useMatrixStore } from "@/src/stores";
import { useAuth } from "@/src/stores/authStore";
import {useColors} from "@/src/hooks/useColors";
export default function MusicScreen() {
const {colors} = useColors();
const { authenticatedUser } = useAuth();
const musicConfig = useMatrixStore((s) => s.matrixState.music);
const updateMusicConfig = useMatrixStore((s) => s.updateMusicConfig);
const hasSpotify = !!authenticatedUser?.spotifyConfig;
return (
<ThemedBackground>
<ThemedHeader>
Music Mode
</ThemedHeader>
<View className="flex-1 justify-between">
<View>
<ThemedHeader subtitle="Visualisiere deine Musik">
Musik Modus
</ThemedHeader>
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-6 mt-4">
<View className="items-center mb-6">
<View className="w-16 h-16 rounded-full bg-primary/10 dark:bg-primary-light/10 items-center justify-center mb-3">
<Feather name="music" size={32} color={colors.primary} />
</View>
<Text className="text-base font-medium text-onSurface dark:text-onSurface-dark">
Musik Visualisierung
</Text>
{!hasSpotify && (
<Text className="text-sm text-muted dark:text-muted-dark text-center mt-2">
Verbinde Spotify in den Einstellungen
</Text>
)}
</View>
<ThemedCheckbox
label="Vollbild-Modus"
description="Zeigt die Visualisierung im Vollbild"
value={musicConfig.fullscreen}
onValueChange={(fullscreen) => updateMusicConfig({ fullscreen })}
/>
</View>
</View>
<SaveToMatrixButton mode="music" />
</View>
</ThemedBackground>
);
}
+41 -35
View File
@@ -1,62 +1,68 @@
import ThemedBackground from "@/src/components/themed/ThemedBackground";
import ThemedTextInput from "@/src/components/themed/ThemedTextInput";
import ThemedButton from "@/src/components/themed/ThemedButton";
import ColorSelector from "@/src/components/themed/ColorSelector";
import {View} from "react-native";
import {View, Text} from "react-native";
import ThemedSegmentedButtons from "@/src/components/themed/ThemedSegmentedButtons";
import { MatrixState } from '@/src/model/User';
import { useMatrixStore } from "@/src/stores";
import ThemedHeader from "@/src/components/themed/ThemedHeader";
import SaveToMatrixButton from "@/src/components/SaveToMatrixButton";
type TextProps = MatrixState['text'];
export default function TextScreen() {
const textConfig = useMatrixStore((s) => s.matrixState.text);
const updateTextConfig = useMatrixStore((s) => s.updateTextConfig);
const setGlobalMode = useMatrixStore((s) => s.setGlobalMode);
const matrixState = useMatrixStore((s) => s.matrixState);
const updateTextProp = (prop: Partial<TextProps>) => {
updateTextConfig(prop);
setGlobalMode('text');
};
const handleSendToMatrix = () => {
console.log("Sende an Matrix:", matrixState);
};
return (
<ThemedBackground>
<View className="flex-1 justify-between p-5">
<View className="flex-1 justify-between">
<View className="gap-4">
<ThemedTextInput
label="Text"
value={textConfig.text}
onChangeText={(text) => updateTextProp({ text })}
/>
<ThemedHeader subtitle="Zeige Text auf deiner Matrix an">
Text Modus
</ThemedHeader>
<ColorSelector
onSelect={(color) => updateTextProp({ color })}
defaultColor={textConfig.color}
/>
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5 gap-4">
<ThemedTextInput
label="Dein Text"
value={textConfig.text}
onChangeText={(text) => updateTextProp({ text })}
className="my-0"
/>
<ThemedSegmentedButtons
value={textConfig.align}
onValueChange={(align) => updateTextProp({ align })}
options={{
left: 'Links',
center: 'Mitte',
right: 'Rechts',
}}
/>
<View>
<Text className="text-sm font-medium text-muted dark:text-muted-dark mb-2">
Textfarbe
</Text>
<ColorSelector
onSelect={(color) => updateTextProp({ color })}
defaultColor={textConfig.color}
/>
</View>
<View>
<Text className="text-sm font-medium text-muted dark:text-muted-dark mb-2">
Ausrichtung
</Text>
<ThemedSegmentedButtons
value={textConfig.align}
onValueChange={(align) => updateTextProp({ align })}
options={{
left: 'Links',
center: 'Mitte',
right: 'Rechts',
}}
className="my-0"
/>
</View>
</View>
</View>
<View className="pt-5">
<ThemedButton
mode="contained"
onPress={handleSendToMatrix}
title={"An die Matrix senden"}
/>
</View>
<SaveToMatrixButton mode="text" />
</View>
</ThemedBackground>
);
+69 -25
View File
@@ -7,7 +7,7 @@ import SpotifyAuthButton from "@/src/components/SpotifyAuthButton";
import {RestService, Token} from "@/src/services/RestService";
import {useAuth} from "@/src/stores/authStore";
import {View} from "react-native";
import {View, Text} from "react-native";
import ThemedButton from "@/src/components/themed/ThemedButton";
import {useRouter} from "expo-router";
@@ -33,32 +33,76 @@ export default function SettingsScreen() {
return (
<ThemedBackground>
<View className="w-full gap-3 items-center">
<ThemedHeader>Einen wunderschönen guten Tag, {authenticatedUser?.name}</ThemedHeader>
<ChangePasswordFeature/>
<ThemeToggleButton/>
<SpotifyAuthButton
onAuthSuccess={handleAuthSuccess}
jwtToken={jwtToken}
disabled={!!authenticatedUser?.spotifyConfig}
/>
{!!authenticatedUser?.spotifyConfig && ( <ThemedButton mode={"outlined"} title={"Remove Spotify"} onPress={() => {
const rest = new RestService(jwtToken);
rest.removeSpotifyConfig().then((result) => {
console.log("Spotify Login entfernt");
console.log(result);
refreshUser()
})
}}/>)}
<View className="flex-1 gap-6">
<ThemedHeader subtitle="Verwalte dein Konto und App-Einstellungen">
Hallo, {authenticatedUser?.name}
</ThemedHeader>
{/* Erscheinungsbild Section */}
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Erscheinungsbild
</Text>
<View className="flex-row items-center justify-between">
<Text className="text-sm text-muted dark:text-muted-dark">
Dark Mode
</Text>
<ThemeToggleButton />
</View>
</View>
{/* Konto Section */}
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Konto
</Text>
<View className="gap-3">
<ChangePasswordFeature />
</View>
</View>
{/* Integrationen Section */}
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Integrationen
</Text>
<View className="gap-3">
<SpotifyAuthButton
onAuthSuccess={handleAuthSuccess}
jwtToken={jwtToken}
disabled={!!authenticatedUser?.spotifyConfig}
/>
{!!authenticatedUser?.spotifyConfig && (
<ThemedButton
mode="outlined"
title="Spotify trennen"
onPress={() => {
const rest = new RestService(jwtToken);
rest.removeSpotifyConfig().then((result) => {
console.log("Spotify Login entfernt");
console.log(result);
refreshUser();
});
}}
/>
)}
</View>
</View>
{/* Logout am Ende */}
<View className="mt-auto pb-4">
<ThemedButton
mode="outlined"
title="Abmelden"
onPress={() => {
console.log("Button pressed");
logout().then(() => {
router.replace("/login");
});
}}
/>
</View>
</View>
<ThemedButton mode={"outlined"} title={"Logout"} onPress={() => {
console.log("Button pressed");
logout().then(() => {
router.replace("/login");
});
}
}/>
</ThemedBackground>
);
}
+55 -35
View File
@@ -1,12 +1,11 @@
import React, {useEffect, useState} from "react";
import {View} from "react-native";
import ThemedBackground from "../src/components/themed/ThemedBackground";
import Logo from "../src/components/Logo";
import ThemedHeader from "../src/components/themed/ThemedHeader";
import ThemedButton from "../src/components/themed/ThemedButton";
import ThemedTextInput from "../src/components/themed/ThemedTextInput";
import BackButton from "../src/components/BackButton";
import {useAuth} from "@/src/stores/authStore";
import {useRouter} from "expo-router";
@@ -37,47 +36,68 @@ export default function LoginScreen() {
if (isAuthenticated) {
return (
<ThemedBackground>
<Logo/>
<ThemedHeader>Du bist bereits eingeloggt. Was machst'n hier?</ThemedHeader>
<ThemedButton mode="contained" onPress={logout} title={"Logout"} />
<ThemedButton mode="outlined" onPress={() => router.push("/")} title={"Zurück"} />
<ThemedBackground className="items-center justify-center">
<Logo size="large" />
<ThemedHeader centered>Du bist bereits eingeloggt. Was machst'n hier?</ThemedHeader>
<View className="w-full gap-2 mt-4">
<ThemedButton mode="contained" onPress={logout} title={"Logout"} />
<ThemedButton mode="outlined" onPress={() => router.push("/")} title={"Zurück"} />
</View>
</ThemedBackground>
)
}
return (
<ThemedBackground>
<BackButton goBack={router.back}/>
<Logo/>
<ThemedHeader>Hello.</ThemedHeader>
<ThemedTextInput
label="Username"
returnKeyType="next"
value={username}
onChangeText={setUsername}
error={!!error && error?.field === "username" }
errorText={error?.message}
autoCapitalize="none"
/>
<PasswordInput
label="Password"
returnKeyType="done"
value={password}
onChangeText={setPassword}
error={!!error && error?.field === "password" }
errorText={error?.message}
autoComplete="password"
/>
<ThemedCheckbox
label="Speichern"
value={stayLoggedIn}
onValueChange={setStayLoggedIn}
/>
<View className="flex-1 justify-center">
<View className="items-center mb-8">
<Logo size="large" />
<ThemedHeader centered subtitle="Melde dich an, um fortzufahren">
Willkommen zurück
</ThemedHeader>
</View>
<ThemedButton mode="outlined" onPress={onLoginPressed} title={"Login"} />
<ThemeToggleButton />
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-6 gap-2">
<ThemedTextInput
label="Username"
returnKeyType="next"
value={username}
onChangeText={setUsername}
error={!!error && error?.field === "username"}
errorText={error?.message}
autoCapitalize="none"
/>
<PasswordInput
label="Password"
returnKeyType="done"
value={password}
onChangeText={setPassword}
error={!!error && error?.field === "password"}
errorText={error?.message}
autoComplete="password"
/>
<ThemedCheckbox
label="Angemeldet bleiben"
description="Du wirst nicht automatisch ausgeloggt"
value={stayLoggedIn}
onValueChange={setStayLoggedIn}
/>
<ThemedButton
mode="contained"
onPress={onLoginPressed}
title={"Anmelden"}
className="mt-4"
/>
</View>
<View className="items-center mt-8">
<ThemeToggleButton />
</View>
</View>
</ThemedBackground>
);
}
+1
View File
@@ -2,3 +2,4 @@
@tailwind components;
@tailwind utilities;
-29
View File
@@ -1,29 +0,0 @@
import React from "react";
import { Image, TouchableOpacity } from "react-native";
import { getStatusBarHeight } from "react-native-status-bar-height";
import { useColors } from "@/src/hooks/useColors";
type Props = {
goBack: () => void;
className?: string;
};
export default function BackButton({ goBack, className }: Props) {
const { colors } = useColors();
const statusBarHeight = getStatusBarHeight();
return (
<TouchableOpacity
onPress={goBack}
className={`absolute left-1 ${className || ''}`}
style={{ top: 10 + statusBarHeight }}
>
<Image
className="w-6 h-6"
style={{ tintColor: colors.onBackground }}
source={require("../../assets/items/back.png")}
/>
</TouchableOpacity>
);
}
+10 -15
View File
@@ -41,29 +41,24 @@ export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswo
};
return (
<View className="p-5 rounded-xl self-center w-full max-w-[400px] bg-surface dark:bg-surface-dark">
<View className="p-6 rounded-2xl self-center w-full max-w-[400px] bg-surface dark:bg-surface-dark">
<Text
variant="titleMedium"
className="text-lg mb-2.5 text-onSurface dark:text-onSurface-dark"
className="text-lg font-semibold mb-4 text-onSurface dark:text-onSurface-dark"
>
Passwort ändern
</Text>
{apiResponse && apiResponse.data?.message && (
<View
className={`my-2 p-3 rounded-lg ${apiResponse.ok ? 'bg-success' : 'bg-error'}`}
>
<Text
variant="bodyMedium"
className="text-white"
>
<View className={`my-3 p-4 rounded-xl ${apiResponse.ok ? 'bg-success' : 'bg-error'}`}>
<Text variant="bodyMedium" className="text-white font-medium">
{apiResponse.data.message}
</Text>
</View>
)}
{!apiResponse?.ok && (
<>
<View className="gap-2">
<PasswordInput
label="Neues Passwort"
value={password}
@@ -80,16 +75,16 @@ export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswo
returnKeyType="go"
onSubmitEditing={handleConfirm}
/>
</>
</View>
)}
<View className="flex-row justify-end gap-2.5 mt-4">
<View className="flex-row justify-end gap-3 mt-5">
{apiResponse?.ok ? (
<ThemedButton mode="contained" onPress={onCancel} title={"Schließen"} style={{flex: 1}} />
<ThemedButton mode="contained" onPress={onCancel} title="Schließen" className="flex-1" />
) : (
<>
<ThemedButton mode="elevated" onPress={onCancel} title={"Abbrechen"} />
<ThemedButton mode="contained" onPress={handleConfirm} title={"Bestätigen"} />
<ThemedButton mode="outlined" onPress={onCancel} title="Abbrechen" />
<ThemedButton mode="contained" onPress={handleConfirm} title="Bestätigen" />
</>
)}
</View>
+9 -3
View File
@@ -1,8 +1,9 @@
import React, {useState} from 'react';
import { Button, View } from 'react-native';
import { View } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import ImageViewer from "@/src/components/ImageViewer";
import {ImagePickerSuccessResult} from "expo-image-picker/src/ImagePicker.types";
import ThemedButton from "@/src/components/themed/ThemedButton";
export interface Props {
onSuccess: (result: ImagePickerSuccessResult) => void;
@@ -40,8 +41,13 @@ export default function CustomImagePicker({onSuccess, onFailure, onCanceled}: Pr
};
return (
<View className="flex-1 items-center justify-center">
<Button title="Pick an image from camera roll" onPress={pickImage}/>
<View className="flex-1 items-center justify-center gap-4">
<ThemedButton
mode="contained"
title="Bild auswählen"
onPress={pickImage}
icon="image"
/>
{image && <ImageViewer imgSource={{uri: image}}/>}
</View>
);
+16 -6
View File
@@ -1,16 +1,26 @@
import React from "react";
import { Image } from "react-native";
import { Image, View } from "react-native";
type Props = {
className?: string;
size?: "small" | "medium" | "large";
};
export default function Logo({ className }: Props) {
export default function Logo({ className, size = "medium" }: Props) {
const sizeClasses = {
small: "w-16 h-16",
medium: "w-28 h-28",
large: "w-40 h-40",
};
return (
<Image
source={require("../../assets/items/logo.png")}
className={`w-28 h-28 mb-2 ${className || ''}`}
/>
<View className={`items-center justify-center mb-4 ${className || ''}`}>
<Image
source={require("../../assets/items/logo.png")}
className={`${sizeClasses[size]} rounded-3xl shadow-glow`}
resizeMode="contain"
/>
</View>
);
}
+17 -22
View File
@@ -8,22 +8,15 @@ export default function PasswordInput(props: ThemedTextInputProps) {
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
const inputRef = useRef<TextInput>(null);
const isWeb = Platform.OS === "web";
const handleCursorReset = () => {
if (cursorPosition === null) return;
if (Platform.OS === "web") {
setTimeout(() => {
const inputElement = document.activeElement as HTMLInputElement;
if (inputElement) {
inputElement.setSelectionRange(cursorPosition, cursorPosition);
}
}, 0);
} else {
setTimeout(() => {
inputRef.current?.setNativeProps({
selection: { start: cursorPosition, end: cursorPosition },
});
}, 0);
}
if (cursorPosition === null || isWeb) return;
setTimeout(() => {
inputRef.current?.setNativeProps({
selection: { start: cursorPosition, end: cursorPosition },
});
}, 0);
};
return (
@@ -40,13 +33,15 @@ export default function PasswordInput(props: ThemedTextInputProps) {
setCursorPosition(e.nativeEvent.selection.start);
}}
right={
<PaperTextInput.Icon
icon={hidePass ? "eye" : "eye-off"}
onPress={() => {
setHidePass(!hidePass);
handleCursorReset();
}}
/>
!isWeb ? (
<PaperTextInput.Icon
icon={hidePass ? "eye" : "eye-off"}
onPress={() => {
setHidePass(!hidePass);
handleCursorReset();
}}
/>
) : undefined
}
/>
);
+73
View File
@@ -0,0 +1,73 @@
import React, { useState } from "react";
import { View, Text } from "react-native";
import ThemedButton from "@/src/components/themed/ThemedButton";
import { useMatrixStore } from "@/src/stores";
import { useAuth } from "@/src/stores/authStore";
import { RestService } from "@/src/services/RestService";
import { MatrixState } from "@/src/model/User";
interface SaveToMatrixButtonProps {
mode: MatrixState['global']['mode'];
className?: string;
}
export default function SaveToMatrixButton({ mode, className }: SaveToMatrixButtonProps) {
const { token } = useAuth();
const setGlobalMode = useMatrixStore((s) => s.setGlobalMode);
const [saving, setSaving] = useState(false);
const [feedback, setFeedback] = useState<{ type: 'success' | 'error', message: string } | null>(null);
const handleSave = async () => {
setSaving(true);
setFeedback(null);
try {
// Setze den globalen Mode auf den aktuellen Screen
setGlobalMode(mode);
// Warte kurz, damit der State aktualisiert ist
await new Promise(resolve => setTimeout(resolve, 50));
// Hole den aktualisierten State
const updatedState = useMatrixStore.getState().matrixState;
const response = await new RestService(token).updateLastState(updatedState);
if (response.ok) {
setFeedback({ type: 'success', message: 'Gespeichert!' });
console.log("Matrix State gespeichert:", updatedState);
} else {
setFeedback({ type: 'error', message: 'Fehler beim Speichern' });
}
} catch (error) {
console.error("Fehler beim Speichern:", error);
setFeedback({ type: 'error', message: 'Verbindungsfehler' });
} finally {
setSaving(false);
// Feedback nach 3 Sekunden ausblenden
setTimeout(() => setFeedback(null), 3000);
}
};
return (
<View className={`gap-2 ${className || ''}`}>
{feedback && (
<View className={`p-3 rounded-xl ${feedback.type === 'success' ? 'bg-success/20' : 'bg-error/20'}`}>
<Text className={`text-center text-sm font-medium ${feedback.type === 'success' ? 'text-success' : 'text-error'}`}>
{feedback.message}
</Text>
</View>
)}
<ThemedButton
mode="contained"
onPress={handleSave}
title={saving ? "Speichern..." : "An Matrix senden"}
icon="send"
disabled={saving}
/>
</View>
);
}
+9 -15
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { View, Dimensions } from 'react-native';
import { View, useWindowDimensions } from 'react-native';
import ColorPicker, { Panel1, Swatches, Preview, HueSlider, ColorFormatsObject } from 'reanimated-color-picker';
import ThemedButton from "@/src/components/themed/ThemedButton";
import ThemedColorPickerButton from "@/src/components/themed/ThemedColorPickerButton";
@@ -14,8 +14,9 @@ interface ColorSelectorProps {
export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect }: ColorSelectorProps) {
const [isModalVisible, setIsModalVisible] = useState(false);
const [pickerHex, setPickerHex] = useState(() => rgbToHex(defaultColor));
const { width } = useWindowDimensions();
const isSmallScreen = width < 400;
const openModal = () => {
setPickerHex(rgbToHex(defaultColor));
@@ -32,9 +33,6 @@ export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect
closeModal();
};
const { width } = Dimensions.get('window');
const modalWidth = Math.min(350, width * 0.9);
return (
<View>
<ThemedColorPickerButton
@@ -46,23 +44,19 @@ export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect
isVisible={isModalVisible}
onClose={closeModal}
>
<View
className="p-5 rounded-xl self-center items-center bg-surface dark:bg-surface-dark"
style={{ width: modalWidth }}
>
<View className={`p-5 rounded-2xl self-center items-center bg-surface dark:bg-surface-dark ${isSmallScreen ? 'w-[90%]' : 'w-[350px]'}`}>
<ColorPicker
style={{ width: '100%' }}
value={pickerHex}
onComplete={(color: ColorFormatsObject) => setPickerHex(color.hex)}
>
<Preview style={{ marginBottom: 15 }} />
<Panel1 style={{ marginBottom: 15 }} />
<HueSlider style={{ marginBottom: 15 }} />
<Swatches style={{ marginTop: 10 }} />
<Preview style={{ marginBottom: 16, borderRadius: 12 }} />
<Panel1 style={{ marginBottom: 16, borderRadius: 12 }} />
<HueSlider style={{ marginBottom: 16, borderRadius: 20 }} />
<Swatches style={{ marginTop: 8 }} />
</ColorPicker>
<ThemedButton
style={{ marginTop: 20, width: '100%' }}
className="mt-5 w-full"
mode="contained"
onPress={handleConfirm}
title={"Bestätigen"}
+2 -1
View File
@@ -25,8 +25,9 @@ const CustomModal = ({ isVisible, onClose, children, onModalDidHide, className }
animationIn="zoomIn"
animationOut="zoomOut"
backdropTransitionOutTiming={0}
backdropOpacity={0.6}
>
<View className={`p-4 rounded-lg ${className || ''}`}>
<View className={`p-4 rounded-2xl ${className || ''}`}>
{children}
</View>
</Modal>
+18 -9
View File
@@ -1,7 +1,6 @@
import React from "react";
import { Button as PaperButton } from "react-native-paper";
import { IconSource } from "react-native-paper/src/components/Icon";
import { useColors } from "@/src/hooks/useColors";
type Props = {
mode: "text" | "outlined" | "contained" | "elevated" | "contained-tonal";
@@ -12,21 +11,31 @@ type Props = {
title: string;
icon?: IconSource;
className?: string;
compact?: boolean;
};
export default function ThemedButton({ mode, style, title, icon, className, ...props }: Props) {
const { colors } = useColors();
export default function ThemedButton({ mode, style, title, icon, className, compact, disabled, ...props }: Props) {
// Basis-Klassen für alle Buttons
const baseClasses = "my-2 rounded-xl";
// Mode-spezifische Klassen
const modeClasses = {
contained: disabled
? "bg-muted dark:bg-muted-dark"
: "bg-primary dark:bg-primary-light",
outlined: "bg-transparent border-2 border-primary dark:border-primary-light",
elevated: "bg-surface dark:bg-surface-dark shadow-card",
"contained-tonal": "bg-primary/20 dark:bg-primary-light/20",
text: "bg-transparent",
};
return (
<PaperButton
className={`my-2.5 py-0.5 ${className || ''}`}
style={[
mode === "outlined" && { backgroundColor: colors.background },
style,
]}
labelStyle={{ fontWeight: "bold", fontSize: 15, lineHeight: 26 }}
className={`${baseClasses} ${modeClasses[mode]} ${compact ? 'py-0' : 'py-1'} ${className || ''}`}
style={style}
mode={mode}
icon={icon}
disabled={disabled}
{...props}
>
{title}
+15 -7
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Text, Pressable } from 'react-native';
import { Text, Pressable, View } from 'react-native';
import Checkbox from 'expo-checkbox';
import { useColors } from '@/src/hooks/useColors';
@@ -8,25 +8,33 @@ type ThemedCheckboxProps = {
value: boolean;
onValueChange: (newValue: boolean) => void;
className?: string;
description?: string;
};
const ThemedCheckbox = ({ label, value, onValueChange, className }: ThemedCheckboxProps) => {
const ThemedCheckbox = ({ label, value, onValueChange, className, description }: ThemedCheckboxProps) => {
const { colors } = useColors();
return (
<Pressable
onPress={() => onValueChange(!value)}
className={`flex-row items-center my-3 ${className || ''}`}
className={`flex-row items-center my-2 py-3 rounded-xl ${value ? 'bg-primary/10 dark:bg-primary-light/10' : ''} active:opacity-80 ${className || ''}`}
>
<Checkbox
className="mr-2"
className="mr-3 w-6 h-6 rounded-md"
value={value}
onValueChange={onValueChange}
color={value ? colors.primary : undefined}
/>
<Text className="text-sm text-onSurface dark:text-onSurface-dark">
{label}
</Text>
<View className="flex-1">
<Text className="text-base font-medium text-onSurface dark:text-onSurface-dark">
{label}
</Text>
{description && (
<Text className="text-sm text-muted dark:text-muted-dark mt-0.5">
{description}
</Text>
)}
</View>
</Pressable>
);
};
@@ -1,31 +1,30 @@
import React from 'react';
import { Pressable, View, Text, StyleProp, ViewStyle } from 'react-native';
import { Pressable, View, Text } from 'react-native';
type Props = {
color: [number, number, number];
onPress: () => void;
title?: string;
style?: StyleProp<ViewStyle>;
className?: string;
};
const ThemedColorPickerButton = ({ color, onPress, title = "Farbe wählen", style, className }: Props) => {
const ThemedColorPickerButton = ({ color, onPress, title = "Farbe wählen", className }: Props) => {
const rgbColor = `rgb(${color.join(',')})`;
return (
<Pressable
className={`flex-row items-center py-3 px-4 rounded-3xl border gap-3
className={`flex-row items-center py-3 px-4 rounded-xl border-2 gap-3
bg-surface dark:bg-surface-dark
border-outline dark:border-outline-dark
active:opacity-80
${className || ''}`}
style={style}
onPress={onPress}
>
<View
className="w-6 h-6 rounded border border-white/50"
className="w-8 h-8 rounded-lg border-2 border-white/30"
style={{ backgroundColor: rgbColor }}
/>
<Text className="text-onSurface dark:text-onSurface-dark">{title}</Text>
<Text className="text-base font-medium text-onSurface dark:text-onSurface-dark">{title}</Text>
</Pressable>
);
};
+17 -7
View File
@@ -1,19 +1,29 @@
import React from "react";
import { Text } from "react-native-paper";
import { View } from "react-native";
type Props = {
children: React.ReactNode;
className?: string;
subtitle?: string;
centered?: boolean;
};
export default function ThemedHeader({ children, className, ...props }: Props) {
export default function ThemedHeader({ children, className, subtitle, centered = false, ...props }: Props) {
return (
<Text
className={`text-xl font-bold py-3 ${className || ''}`}
{...props}
>
{children}
</Text>
<View className={`mb-2 ${centered ? 'items-center' : ''}`}>
<Text
className={`text-2xl font-bold tracking-tight py-2 text-onSurface dark:text-onSurface-dark ${className || ''}`}
{...props}
>
{children}
</Text>
{subtitle && (
<Text className="text-sm text-muted dark:text-muted-dark mt-1">
{subtitle}
</Text>
)}
</View>
);
}
+8 -5
View File
@@ -16,22 +16,25 @@ const ThemedTextInput = forwardRef<any, ThemedTextInputProps>(
}
return (
<View className={`w-full my-3 ${className || ''}`}>
<View className={`w-full my-2 ${className || ''}`}>
<Input
underlineColor="transparent"
mode="outlined"
className="rounded-xl bg-surface dark:bg-surface-dark"
{...props}
ref={ref}
/>
{description && !error ? (
<Text className="text-sm pt-2 text-onSurface dark:text-onSurface-dark">
<Text className="text-sm pt-2 px-1 text-muted dark:text-muted-dark">
{description}
</Text>
) : null}
{error && (
<Text className="text-sm pt-2 text-error">
{errorText}
</Text>
<View className="flex-row items-center pt-2 px-1">
<Text className="text-sm text-error font-medium">
{errorText}
</Text>
</View>
)}
</View>
);
+80 -24
View File
@@ -1,54 +1,84 @@
// Single source of truth for all theme colors
// Imported by: tailwind.config.js, useColors.ts, theme.ts
// Modern, vibrant color palette with better contrast
const colors = {
primary: {
DEFAULT: '#6750A4',
light: '#7F67BE',
dark: '#4F378B',
DEFAULT: '#6366F1', // Indigo - modern & vibrant
light: '#818CF8',
dark: '#4F46E5',
},
secondary: {
DEFAULT: '#625B71',
light: '#7A7289',
dark: '#4A4458',
DEFAULT: '#8B5CF6', // Purple accent
light: '#A78BFA',
dark: '#7C3AED',
},
tertiary: {
DEFAULT: '#EC4899', // Pink for highlights
light: '#F472B6',
dark: '#DB2777',
},
surface: {
DEFAULT: '#FFFBFE',
dark: '#1C1B1F',
DEFAULT: '#FAFAFA',
dark: '#18181B',
elevated: '#FFFFFF',
elevatedDark: '#27272A',
},
background: {
DEFAULT: '#FFFBFE',
dark: '#1C1B1F',
DEFAULT: '#F4F4F5',
dark: '#09090B',
},
card: {
DEFAULT: '#FFFFFF',
dark: '#1C1C1E',
},
error: {
DEFAULT: '#B3261E',
light: '#DC362E',
dark: '#8C1D18',
DEFAULT: '#EF4444',
light: '#F87171',
dark: '#DC2626',
},
success: {
DEFAULT: '#4CAF50',
light: '#66BB6A',
dark: '#388E3C',
DEFAULT: '#10B981', // Emerald green
light: '#34D399',
dark: '#059669',
},
warning: {
DEFAULT: '#F59E0B', // Amber
light: '#FBBF24',
dark: '#D97706',
},
info: {
DEFAULT: '#3B82F6', // Blue
light: '#60A5FA',
dark: '#2563EB',
},
onPrimary: {
DEFAULT: '#FFFFFF',
dark: '#1C1B1F',
dark: '#FFFFFF',
},
onSecondary: {
DEFAULT: '#FFFFFF',
dark: '#1C1B1F',
dark: '#FFFFFF',
},
onSurface: {
DEFAULT: '#1C1B1F',
dark: '#E6E1E5',
DEFAULT: '#18181B',
dark: '#FAFAFA',
},
onBackground: {
DEFAULT: '#1C1B1F',
dark: '#E6E1E5',
DEFAULT: '#27272A',
dark: '#E4E4E7',
},
outline: {
DEFAULT: '#79747E',
dark: '#938F99',
DEFAULT: '#D4D4D8',
dark: '#3F3F46',
},
muted: {
DEFAULT: '#71717A',
dark: '#A1A1AA',
},
accent: {
DEFAULT: '#F0ABFC', // Light purple glow
dark: '#C026D3',
},
};
@@ -62,19 +92,32 @@ const lightColors = {
secondary: colors.secondary.DEFAULT,
secondaryLight: colors.secondary.light,
secondaryDark: colors.secondary.dark,
tertiary: colors.tertiary.DEFAULT,
tertiaryLight: colors.tertiary.light,
tertiaryDark: colors.tertiary.dark,
surface: colors.surface.DEFAULT,
surfaceElevated: colors.surface.elevated,
background: colors.background.DEFAULT,
card: colors.card.DEFAULT,
error: colors.error.DEFAULT,
errorLight: colors.error.light,
errorDark: colors.error.dark,
success: colors.success.DEFAULT,
successLight: colors.success.light,
successDark: colors.success.dark,
warning: colors.warning.DEFAULT,
warningLight: colors.warning.light,
warningDark: colors.warning.dark,
info: colors.info.DEFAULT,
infoLight: colors.info.light,
infoDark: colors.info.dark,
onPrimary: colors.onPrimary.DEFAULT,
onSecondary: colors.onSecondary.DEFAULT,
onSurface: colors.onSurface.DEFAULT,
onBackground: colors.onBackground.DEFAULT,
outline: colors.outline.DEFAULT,
muted: colors.muted.DEFAULT,
accent: colors.accent.DEFAULT,
};
const darkColors = {
@@ -84,19 +127,32 @@ const darkColors = {
secondary: colors.secondary.light,
secondaryLight: colors.secondary.DEFAULT,
secondaryDark: colors.secondary.dark,
tertiary: colors.tertiary.light,
tertiaryLight: colors.tertiary.DEFAULT,
tertiaryDark: colors.tertiary.dark,
surface: colors.surface.dark,
surfaceElevated: colors.surface.elevatedDark,
background: colors.background.dark,
card: colors.card.dark,
error: colors.error.light,
errorLight: colors.error.DEFAULT,
errorDark: colors.error.dark,
success: colors.success.light,
successLight: colors.success.DEFAULT,
successDark: colors.success.dark,
warning: colors.warning.light,
warningLight: colors.warning.DEFAULT,
warningDark: colors.warning.dark,
info: colors.info.light,
infoLight: colors.info.DEFAULT,
infoDark: colors.info.dark,
onPrimary: colors.onPrimary.dark,
onSecondary: colors.onSecondary.dark,
onSurface: colors.onSurface.dark,
onBackground: colors.onBackground.dark,
outline: colors.outline.dark,
muted: colors.muted.dark,
accent: colors.accent.dark,
};
module.exports.lightColors = lightColors;
+10 -1
View File
@@ -1,6 +1,6 @@
import axios, {AxiosInstance, Method} from 'axios';
import {makeRedirectUri} from "expo-auth-session";
import {SpotifyConfig, User} from "@/src/model/User";
import {MatrixState, SpotifyConfig, User} from "@/src/model/User";
import {Platform} from "react-native";
const API_URL = process.env.EXPO_PUBLIC_API_URL;
@@ -101,6 +101,15 @@ class RestService {
);
}
async updateLastState(lastState: MatrixState): Promise<ApiResponse<{ message: string }>> {
return this.request<ApiResponse<{ message: string }>>(
'PUT',
'/user/me/state',
{ lastState },
{'Content-Type': 'application/json'}
);
}
async sendPayloadToSocket(userId: string, payload: object): Promise<any> {
return this.request(
'POST',
+14
View File
@@ -23,6 +23,20 @@ module.exports = {
"xl": "12px",
"2xl": "16px",
"3xl": "24px",
"4xl": "32px",
},
boxShadow: {
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
'glow': '0 0 15px rgba(99, 102, 241, 0.5)',
'glow-lg': '0 0 30px rgba(99, 102, 241, 0.6)',
'card': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
'card-hover': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-primary': 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
'gradient-secondary': 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%)',
'gradient-dark': 'linear-gradient(180deg, #18181B 0%, #09090B 100%)',
},
},
},