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="nether-portal-calculator" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="todo" options={{ headerShown: false }} />
|
<Stack.Screen name="todo" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="saved-coordinates" options={{ headerShown: false }} />
|
<Stack.Screen name="saved-coordinates" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="motd" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</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,
|
fontSize: 18,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
fontFamily: "Minecraft"
|
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