feat: Add MOTD-page
Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
This commit is contained in:
parent
479f030b45
commit
62681a5c6b
4 changed files with 486 additions and 0 deletions
|
@ -16,6 +16,7 @@ export default function RootLayout() {
|
|||
<Stack.Screen name="nether-portal-calculator" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="todo" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="saved-coordinates" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="motd" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
377
app/motd.tsx
Normal file
377
app/motd.tsx
Normal file
|
@ -0,0 +1,377 @@
|
|||
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>
|
||||
);
|
||||
}
|
108
app/styles.ts
108
app/styles.ts
|
@ -354,5 +354,113 @@ export const styles = StyleSheet.create({
|
|||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Minecraft"
|
||||
},
|
||||
|
||||
// MOTD
|
||||
textInput: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: 10,
|
||||
padding: 15,
|
||||
fontSize: 16,
|
||||
color: "#fff",
|
||||
marginBottom: 20,
|
||||
minHeight: 80,
|
||||
textAlignVertical: "top",
|
||||
borderColor: "#4CAF50",
|
||||
borderWidth: 1
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
color: "#fff",
|
||||
marginBottom: 10,
|
||||
marginTop: 15
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
marginBottom: 15
|
||||
},
|
||||
colorButton: {
|
||||
backgroundColor: "#1e88e5",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
margin: 5
|
||||
},
|
||||
formatButton: {
|
||||
backgroundColor: "#ffb300",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
margin: 5
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontWeight: "bold",
|
||||
fontSize: 14
|
||||
},
|
||||
previewContainer: {
|
||||
backgroundColor: "#000",
|
||||
padding: 15,
|
||||
borderRadius: 10,
|
||||
minHeight: 60,
|
||||
justifyContent: "center",
|
||||
marginBottom: 20,
|
||||
borderColor: "#555",
|
||||
borderWidth: 1,
|
||||
},
|
||||
previewBaseText: {
|
||||
fontSize: 18,
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: "#4CAF50",
|
||||
paddingVertical: 15,
|
||||
borderRadius: 10,
|
||||
alignItems: "center",
|
||||
marginBottom: 20
|
||||
},
|
||||
saveButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 18,
|
||||
fontWeight: "bold"
|
||||
},
|
||||
noMotdsText: {
|
||||
color: "#ccc",
|
||||
textAlign: "center",
|
||||
fontStyle: "italic",
|
||||
marginTop: 10
|
||||
},
|
||||
savedMotdsContainer: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: 10,
|
||||
padding: 10
|
||||
},
|
||||
savedMotdItem: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
padding: 10,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "#333"
|
||||
},
|
||||
savedMotdText: {
|
||||
color: "#eee",
|
||||
fontSize: 16,
|
||||
flexShrink: 1
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: "#e53935",
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 5
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: "#fff",
|
||||
fontWeight: "bold"
|
||||
}
|
||||
});
|
BIN
assets/images/motd.png
Normal file
BIN
assets/images/motd.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
Loading…
Add table
Reference in a new issue