chore: Refactor UI components and improve styling across various screens
This commit is contained in:
+22
-12
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user