Skip to content

Implementation of import/export functionality for favourite servers list #307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
toolchain: nightly-2025-02-21 # Specific nightly version that works with dll-syringe, remove when it gets fixed so we can use latest

- name: Install frontend dependencies
run: yarn install
Expand Down
34 changes: 31 additions & 3 deletions src/containers/Settings/Tab/Advanced.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { t } from "i18next";
import { Pressable, StyleSheet, View } from "react-native";
import { Pressable, StyleSheet, View, TouchableOpacity } from "react-native";
import CheckBox from "../../../components/CheckBox";
import Text from "../../../components/Text";
import { useGenericPersistentState } from "../../../states/genericStates";
import { useTheme } from "../../../states/theme";
import { sc } from "../../../utils/sizeScaler";
import { exportFavoriteListFile, importFavoriteListFile } from "../../../utils/game";

const Advanced = () => {
const { theme } = useTheme();
const { shouldUpdateDiscordStatus, toggleDiscordStatus } =
useGenericPersistentState();

return (
<View
style={{
Expand Down Expand Up @@ -44,12 +44,40 @@ const Advanced = () => {
)}`}
</Text>
</Pressable>
<TouchableOpacity
style={[
styles.importButton,
{
backgroundColor: `${theme.primary}BB`,
borderColor: theme.primary
}
]}
onPress={() => exportFavoriteListFile()}
>
<Text semibold color={"#FFFFFF"} size={2}>
{t("settings_export_favorite_list_file")}
</Text>
</TouchableOpacity>

<TouchableOpacity
style={[
styles.importButton,
{
backgroundColor: `${theme.primary}BB`,
borderColor: theme.primary
}
]}
onPress={() => importFavoriteListFile()}
>
<Text semibold color={"#FFFFFF"} size={2}>
{t("settings_import_favorite_list_file")}
</Text>
</TouchableOpacity>
</View>
<View style={styles.pathInputContainer}></View>
</View>
);
};

const styles = StyleSheet.create({
pathInputContainer: {
flexDirection: "row",
Expand Down
12 changes: 12 additions & 0 deletions src/locales/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,16 @@ export default {
reconnect: "Reconnect",
settings_advanced_discord_status_requires_restart:
"(Requires restarting the game to take action)",
settings_export_favorite_list_file: "Export favorites list to file",
settings_import_favorite_list_file: "Import favorites list from file",
export_no_servers_description: "You don't have any favourite servers to export.",
export_successful_title: "Export Complete",
export_successful_description: "Server list exported successfully.",
export_failed_title: "Export Failed",
export_failed_description: "An error occurred while exporting your favorite servers.",
import_successful_title: "Import Complete",
import_successful_description: "Server list imported successfully.",
import_failed_title: "Import Failed",
import_failed_description: "An error occurred while importing your favorite servers.",
import_invalid_data_description: "The selected file contains invalid data. Please select a valid favorites list file.",
};
12 changes: 12 additions & 0 deletions src/locales/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,16 @@ export default {
reconnect: "Reconectar",
settings_advanced_discord_status_requires_restart:
"(Requer reiniciar o jogo para aplicar)",
settings_export_favorite_list_file: "Exportar lista de favoritos para um arquivo",
settings_import_favorite_list_file: "Importar lista de favoritos de um arquivo",
export_no_servers_description: "Você não tem servidores favoritos para exportar.",
export_successful_title: "Exportação Concluída",
export_successful_description: "Lista de servidores exportada com sucesso.",
export_failed_title: "Falha na Exportação",
export_failed_description: "Ocorreu um erro ao exportar seus servidores favoritos.",
import_successful_title: "Importação Concluída",
import_successful_description: "Lista de servidores importada com sucesso.",
import_failed_title: "Falha na Importação",
import_failed_description: "Ocorreu um erro ao importar seus servidores favoritos.",
import_invalid_data_description: "O arquivo selecionado contém dados inválidos. Selecione um arquivo de lista de favoritos válido.",
};
155 changes: 154 additions & 1 deletion src/utils/game.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fs, invoke, path, process, shell } from "@tauri-apps/api";
import { exists } from "@tauri-apps/api/fs";
import { exists, writeTextFile, readTextFile } from "@tauri-apps/api/fs";
import { t } from "i18next";
import { invoke_rpc } from "../api/rpc";
import { ResourceInfo, validFileChecksums } from "../constants/app";
Expand All @@ -12,6 +12,9 @@ import { Log } from "./logger";
import { sc } from "./sizeScaler";
import { Server } from "./types";
import { useGenericPersistentState } from "../states/genericStates";
import { save, open } from '@tauri-apps/api/dialog';
import { useNotification } from "../states/notification";
import { fetchServers } from "../utils/helpers";

export const copySharedFilesIntoGameFolder = async () => {
const { gtasaPath } = useSettings.getState();
Expand Down Expand Up @@ -315,3 +318,153 @@ export const checkDirectoryValidity = async (

return true;
};

export const exportFavoriteListFile = async () => {
const { favorites } = usePersistentServers.getState();
const { showMessageBox, hideMessageBox } = useMessageBox.getState();

if (!favorites.length) {
showMessageBox({
title: t("export_failed_title"),
description: t("export_no_servers_description"),
buttons: [
{
title: "OK",
onPress: () => hideMessageBox(),
},
],
});
return;
}

try {
const exportData = {
version: 1,
servers: favorites.map(server => ({
ip: server.ip,
port: server.port,
name: server.hostname,
password: server.password || ""
}))
};

const jsonString = JSON.stringify(exportData, null, 2);

const savePath = await save({
filters: [{
name: 'JSON',
extensions: ['json']
}],
defaultPath: 'omp_servers.json'
});

if (savePath) {
await writeTextFile(savePath, jsonString);

const { showNotification } = useNotification.getState();
showNotification(
t("export_successful_title"),
t("export_successful_description")
);
}
} catch (error) {
Log.debug("Error exporting servers:", error);
showMessageBox({
title: t("export_failed_title"),
description: t("export_failed_description"),
buttons: [
{
title: "OK",
onPress: () => hideMessageBox(),
},
],
});
}
};

export const importFavoriteListFile = async () => {
const { showMessageBox, hideMessageBox } = useMessageBox.getState();

try {
const selected = await open({
multiple: false,
filters: [{
name: 'JSON',
extensions: ['json']
}]
});
if (!selected) return;
const fileContent = await readTextFile(selected as string);
const { addToFavorites } = usePersistentServers.getState();
const { showNotification } = useNotification.getState();

try {
const data = JSON.parse(fileContent);

if (!data.servers || !Array.isArray(data.servers)) {
throw new Error("Invalid file format: missing servers array");
}

let importCount = 0;

for (const importedServer of data.servers) {
if (importedServer.ip && importedServer.port) {
const tempHostname = `${importedServer.ip}:${importedServer.port}`;
const serverInfo: Server = {
ip: importedServer.ip,
port: Number(importedServer.port),
hostname: importedServer.name || tempHostname,
playerCount: 0,
maxPlayers: 0,
gameMode: "-",
language: "-",
hasPassword: !!importedServer.password,
version: "-",
usingOmp: false,
partner: false,
ping: 9999,
players: [],
password: importedServer.password || "",
rules: {} as Server["rules"],
};

addToFavorites(serverInfo);
importCount++;
}
}

fetchServers(true);

showNotification(
t("import_successful_title"),
t("import_successful_description")
);

} catch (parseError) {
showMessageBox({
title: t("import_failed_title"),
description: t("import_invalid_data_description"),
buttons: [
{
title: "OK",
onPress: () => hideMessageBox(),
},
],
});
Log.debug("Error parsing imported file:", parseError);
}

} catch (error) {
showMessageBox({
title: t("import_failed_title"),
description: t("import_failed_description"),
buttons: [
{
title: "OK",
onPress: () => hideMessageBox(),
},
],
});
Log.debug("Error importing servers:", error);
}
};
Loading