377 lines
14 KiB
TypeScript
377 lines
14 KiB
TypeScript
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||
|
import { useRouter } from "expo-router";
|
||
|
import React, { useEffect, useState } from "react";
|
||
|
import {
|
||
|
Alert,
|
||
|
ImageBackground,
|
||
|
KeyboardAvoidingView,
|
||
|
Platform,
|
||
|
ScrollView,
|
||
|
Text,
|
||
|
TextInput,
|
||
|
TouchableOpacity,
|
||
|
View
|
||
|
} from "react-native";
|
||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||
|
import { styles } from "./styles";
|
||
|
|
||
|
const motdBgImage = require("../assets/images/motd.png");
|
||
|
|
||
|
// Define a map for Minecraft color codes to React Native colors
|
||
|
const MINECRAFT_COLORS: { [key: string]: string } = {
|
||
|
"0": "#000000", // Black
|
||
|
"1": "#0000AA", // Dark Blue
|
||
|
"2": "#00AA00", // Dark Green
|
||
|
"3": "#00AAAA", // Dark Aqua
|
||
|
"4": "#AA0000", // Dark Red
|
||
|
"5": "#AA00AA", // Dark Purple
|
||
|
"6": "#FFAA00", // Gold
|
||
|
"7": "#AAAAAA", // Gray
|
||
|
"8": "#555555", // Dark Gray
|
||
|
"9": "#5555FF", // Blue
|
||
|
a: "#55FF55", // Green
|
||
|
b: "#55FFFF", // Aqua
|
||
|
c: "#FF5555", // Red
|
||
|
d: "#FF55FF", // Light Purple
|
||
|
e: "#FFFF55", // Yellow
|
||
|
f: "#FFFFFF" // White
|
||
|
};
|
||
|
|
||
|
export default function MotdCreator() {
|
||
|
const router = useRouter();
|
||
|
const [motdText, setMotdText] = useState<string>("");
|
||
|
const [savedMotds, setSavedMotds] = useState<string[]>([]);
|
||
|
const [previewMotdComponents, setPreviewMotdComponents] = useState<
|
||
|
React.ReactNode[]
|
||
|
>([]);
|
||
|
|
||
|
// Define a unique key for AsyncStorage
|
||
|
const STORAGE_KEY = "@minecraft_motd_list";
|
||
|
|
||
|
// Persistence Logic
|
||
|
useEffect(() => {
|
||
|
loadMotds();
|
||
|
}, []);
|
||
|
|
||
|
useEffect(() => {
|
||
|
saveMotds();
|
||
|
}, [savedMotds]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
generateMotdPreview();
|
||
|
}, [motdText]);
|
||
|
|
||
|
// Async function to load MOTDs from AsyncStorage
|
||
|
const loadMotds = async () => {
|
||
|
try {
|
||
|
const jsonValue = await AsyncStorage.getItem(STORAGE_KEY);
|
||
|
if (jsonValue != null) {
|
||
|
const parsedMotds = JSON.parse(jsonValue);
|
||
|
if (Array.isArray(parsedMotds)) {
|
||
|
setSavedMotds(parsedMotds);
|
||
|
} else {
|
||
|
console.warn(
|
||
|
"Loaded data is not an array, initializing empty MOTD list."
|
||
|
);
|
||
|
setSavedMotds([]);
|
||
|
}
|
||
|
}
|
||
|
} catch (e) {
|
||
|
console.error("Failed to load MOTDs:", e);
|
||
|
Alert.alert("Error", "Failed to load your MOTD list.");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Async function to save MOTDs to AsyncStorage
|
||
|
const saveMotds = async () => {
|
||
|
try {
|
||
|
const jsonValue = JSON.stringify(savedMotds);
|
||
|
await AsyncStorage.setItem(STORAGE_KEY, jsonValue);
|
||
|
} catch (e) {
|
||
|
console.error("Failed to save MOTDs:", e);
|
||
|
Alert.alert("Error", "Failed to save your MOTD list.");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Function to apply Minecraft color codes
|
||
|
const applyCode = (code: string) => {
|
||
|
setMotdText((prevText) => prevText + "§" + code);
|
||
|
};
|
||
|
|
||
|
// Function to generate a preview of the MOTD with styled Text components
|
||
|
const generateMotdPreview = () => {
|
||
|
const parts: React.ReactNode[] = [];
|
||
|
let currentText = "";
|
||
|
let currentColor: string | undefined = undefined;
|
||
|
let isBold = false;
|
||
|
let isItalic = false;
|
||
|
let isUnderlined = false;
|
||
|
let isStrikethrough = false;
|
||
|
let isObfuscated = false;
|
||
|
|
||
|
const regex = /(§[0-9a-fk-or])|([^§]+)/g;
|
||
|
let match;
|
||
|
|
||
|
while ((match = regex.exec(motdText)) !== null) {
|
||
|
const code = match[1];
|
||
|
const textContent = match[2];
|
||
|
|
||
|
if (currentText.length > 0) {
|
||
|
const style: any = {
|
||
|
color: currentColor || MINECRAFT_COLORS.f
|
||
|
};
|
||
|
if (isBold) style.fontWeight = "bold";
|
||
|
if (isItalic) style.fontStyle = "italic";
|
||
|
if (isUnderlined)
|
||
|
style.textDecorationLine = "underline";
|
||
|
if (isStrikethrough)
|
||
|
style.textDecorationLine = style.textDecorationLine
|
||
|
? style.textDecorationLine + " line-through"
|
||
|
: "line-through";
|
||
|
|
||
|
parts.push(
|
||
|
<Text key={Math.random()} style={style}>
|
||
|
{currentText}
|
||
|
</Text>
|
||
|
);
|
||
|
currentText = ""; // Reset current text
|
||
|
}
|
||
|
|
||
|
if (code) {
|
||
|
const codeChar = code.charAt(1);
|
||
|
|
||
|
// Apply color
|
||
|
if (MINECRAFT_COLORS[codeChar]) {
|
||
|
currentColor = MINECRAFT_COLORS[codeChar];
|
||
|
}
|
||
|
// Apply formatting
|
||
|
switch (codeChar) {
|
||
|
case "l": // Bold
|
||
|
isBold = true;
|
||
|
break;
|
||
|
case "m": // Strikethrough
|
||
|
isStrikethrough = true;
|
||
|
break;
|
||
|
case "n": // Underline
|
||
|
isUnderlined = true;
|
||
|
break;
|
||
|
case "o": // Italic
|
||
|
isItalic = true;
|
||
|
break;
|
||
|
case "k": // Obfuscated
|
||
|
isObfuscated = true; // Mark as obfuscated, but don't change text
|
||
|
break;
|
||
|
case "r": // Reset
|
||
|
currentColor = undefined;
|
||
|
isBold = false;
|
||
|
isItalic = false;
|
||
|
isUnderlined = false;
|
||
|
isStrikethrough = false;
|
||
|
isObfuscated = false;
|
||
|
break;
|
||
|
}
|
||
|
} else if (textContent) {
|
||
|
currentText += textContent;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add any remaining text after the last code or if no codes were found
|
||
|
if (currentText.length > 0) {
|
||
|
const style: any = {
|
||
|
color: currentColor || MINECRAFT_COLORS.f
|
||
|
};
|
||
|
if (isBold) style.fontWeight = "bold";
|
||
|
if (isItalic) style.fontStyle = "italic";
|
||
|
if (isUnderlined) style.textDecorationLine = "underline";
|
||
|
if (isStrikethrough)
|
||
|
style.textDecorationLine = style.textDecorationLine
|
||
|
? style.textDecorationLine + " line-through"
|
||
|
: "line-through";
|
||
|
|
||
|
parts.push(
|
||
|
<Text key={Math.random()} style={style}>
|
||
|
{currentText}
|
||
|
</Text>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
setPreviewMotdComponents(parts);
|
||
|
};
|
||
|
|
||
|
const saveMotd = () => {
|
||
|
if (motdText.trim().length > 0) {
|
||
|
setSavedMotds((prevMotds) => [...prevMotds, motdText.trim()]);
|
||
|
setMotdText(""); // Clear input after saving
|
||
|
} else {
|
||
|
Alert.alert("Empty MOTD", "Please enter some text to save.");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const deleteMotd = (index: number) => {
|
||
|
const motdToDelete = savedMotds[index];
|
||
|
|
||
|
Alert.alert(
|
||
|
"Delete MOTD",
|
||
|
`Are you sure you want to delete "${motdToDelete}"?`,
|
||
|
[
|
||
|
{
|
||
|
text: "Cancel",
|
||
|
onPress: () => console.log("Delete Cancelled"),
|
||
|
style: "cancel"
|
||
|
},
|
||
|
{
|
||
|
text: "Delete",
|
||
|
onPress: () => {
|
||
|
const updatedMotds = savedMotds.filter(
|
||
|
(_, i) => i !== index
|
||
|
);
|
||
|
setSavedMotds(updatedMotds);
|
||
|
console.log("MOTD deleted:", motdToDelete);
|
||
|
},
|
||
|
style: "destructive"
|
||
|
}
|
||
|
],
|
||
|
{ cancelable: true }
|
||
|
);
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<View style={styles.converterScreenRoot}>
|
||
|
<ImageBackground
|
||
|
source={motdBgImage}
|
||
|
style={styles.converterBackgroundImage}
|
||
|
resizeMode="cover"
|
||
|
>
|
||
|
<View style={styles.converterBackgroundOverlay} />
|
||
|
</ImageBackground>
|
||
|
|
||
|
<SafeAreaView style={styles.converterContentWrapper}>
|
||
|
<KeyboardAvoidingView
|
||
|
style={styles.converterContainer}
|
||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||
|
>
|
||
|
<ScrollView
|
||
|
contentContainerStyle={styles.converterScrollContent}
|
||
|
>
|
||
|
{/* Custom Back Button */}
|
||
|
<TouchableOpacity
|
||
|
onPress={() => router.back()}
|
||
|
style={styles.backButton}
|
||
|
>
|
||
|
<Text style={styles.backButtonText}>
|
||
|
← Go Back
|
||
|
</Text>
|
||
|
</TouchableOpacity>
|
||
|
|
||
|
<Text style={styles.converterTitle}>
|
||
|
MOTD Creator
|
||
|
</Text>
|
||
|
|
||
|
{/* MOTD Input */}
|
||
|
<TextInput
|
||
|
style={styles.textInput}
|
||
|
placeholder="Enter your MOTD text here..."
|
||
|
placeholderTextColor="#ccc"
|
||
|
multiline
|
||
|
value={motdText}
|
||
|
onChangeText={setMotdText}
|
||
|
/>
|
||
|
|
||
|
{/* Color Codes */}
|
||
|
<Text style={styles.sectionTitle}>Color Codes:</Text>
|
||
|
<View style={styles.buttonRow}>
|
||
|
{Object.entries(MINECRAFT_COLORS).map(
|
||
|
([codeChar, colorHex]) => (
|
||
|
<TouchableOpacity
|
||
|
key={codeChar}
|
||
|
style={[
|
||
|
styles.colorButton,
|
||
|
{ backgroundColor: colorHex }
|
||
|
]}
|
||
|
onPress={() => applyCode(codeChar)}
|
||
|
>
|
||
|
<Text style={styles.buttonText}>
|
||
|
§{codeChar}
|
||
|
</Text>
|
||
|
</TouchableOpacity>
|
||
|
)
|
||
|
)}
|
||
|
</View>
|
||
|
|
||
|
{/* Formatting Codes */}
|
||
|
<Text style={styles.sectionTitle}>
|
||
|
Formatting Codes:
|
||
|
</Text>
|
||
|
<View style={styles.buttonRow}>
|
||
|
{[
|
||
|
{ code: "k", name: "Obfuscated" },
|
||
|
{ code: "l", name: "Bold" },
|
||
|
{ code: "m", name: "Strikethrough" },
|
||
|
{ code: "n", name: "Underline" },
|
||
|
{ code: "o", name: "Italic" },
|
||
|
{ code: "r", name: "Reset" }
|
||
|
].map((format) => (
|
||
|
<TouchableOpacity
|
||
|
key={format.code}
|
||
|
style={styles.formatButton}
|
||
|
onPress={() => applyCode(format.code)}
|
||
|
>
|
||
|
<Text style={styles.buttonText}>
|
||
|
§{format.code} {format.name}
|
||
|
</Text>
|
||
|
</TouchableOpacity>
|
||
|
))}
|
||
|
</View>
|
||
|
|
||
|
{/* MOTD Preview */}
|
||
|
<Text style={styles.sectionTitle}>MOTD Preview:</Text>
|
||
|
<View style={styles.previewContainer}>
|
||
|
<Text style={styles.previewBaseText}>
|
||
|
{previewMotdComponents}
|
||
|
</Text>
|
||
|
</View>
|
||
|
|
||
|
{/* Save MOTD Button */}
|
||
|
<TouchableOpacity
|
||
|
style={styles.saveButton}
|
||
|
onPress={saveMotd}
|
||
|
>
|
||
|
<Text style={styles.saveButtonText}>Save MOTD</Text>
|
||
|
</TouchableOpacity>
|
||
|
|
||
|
{/* Saved MOTDs */}
|
||
|
<Text style={styles.sectionTitle}>Saved MOTDs:</Text>
|
||
|
{savedMotds.length === 0 ? (
|
||
|
<Text style={styles.noMotdsText}>
|
||
|
No MOTDs saved yet.
|
||
|
</Text>
|
||
|
) : (
|
||
|
<View style={styles.savedMotdsContainer}>
|
||
|
{savedMotds.map((motd, index) => (
|
||
|
<View
|
||
|
key={index}
|
||
|
style={styles.savedMotdItem}
|
||
|
>
|
||
|
<Text style={styles.savedMotdText}>
|
||
|
{motd}
|
||
|
</Text>
|
||
|
<TouchableOpacity
|
||
|
onPress={() => deleteMotd(index)}
|
||
|
style={styles.deleteButton}
|
||
|
>
|
||
|
<Text
|
||
|
style={styles.deleteButtonText}
|
||
|
>
|
||
|
Delete
|
||
|
</Text>
|
||
|
</TouchableOpacity>
|
||
|
</View>
|
||
|
))}
|
||
|
</View>
|
||
|
)}
|
||
|
</ScrollView>
|
||
|
</KeyboardAvoidingView>
|
||
|
</SafeAreaView>
|
||
|
</View>
|
||
|
);
|
||
|
}
|