Skip to content

Commit

Permalink
Add interface settings export/import
Browse files Browse the repository at this point in the history
Issue #197
  • Loading branch information
qu1ck committed Jun 9, 2024
1 parent fa2140a commit 5efef01
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 4 deletions.
12 changes: 12 additions & 0 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use std::fs;

use base64::{engine::general_purpose::STANDARD as b64engine, Engine as _};
use font_loader::system_fonts;
use lava_torrent::torrent::v1::Torrent;
Expand Down Expand Up @@ -266,3 +268,13 @@ pub async fn create_tray(app_handle: tauri::AppHandle) {
.ok();
}
}

#[tauri::command]
pub async fn save_text_file(contents: String, path: String) -> Result<(), String> {
fs::write(path, contents).map_err(|e| format!("Unable to write file: {}", e))
}

#[tauri::command]
pub async fn load_text_file(path: String) -> Result<String, String> {
fs::read_to_string(path).map_err(|e| format!("Unable to read file: {}", e))
}
2 changes: 2 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ fn main() {
commands::pass_to_window,
commands::list_system_fonts,
commands::create_tray,
commands::save_text_file,
commands::load_text_file,
])
.manage(ListenerHandle(Arc::new(RwLock::new(ipc))))
.manage(TorrentCacheHandle::default())
Expand Down
2 changes: 1 addition & 1 deletion src/components/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ function TrackerUpdate(props: { torrent: Torrent }) {

function TransferTable(props: { torrent: Torrent }) {
const seedingTime = secondsToHumanReadableStr(props.torrent.secondsSeeding);
const shareRatio = `${props.torrent.uploadRatio as number} ${seedingTime !== "" ? `(${seedingTime})` : ""}`;
const shareRatio = `${(props.torrent.uploadRatio as number).toFixed(5)} ${seedingTime !== "" ? `(${seedingTime})` : ""}`;

const [ref, rect] = useResizeObserver();

Expand Down
39 changes: 38 additions & 1 deletion src/components/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import type { MantineTheme } from "@mantine/core";
import { ActionIcon, Button, Flex, Kbd, Menu, TextInput, useMantineTheme } from "@mantine/core";
import debounce from "lodash-es/debounce";
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { forwardRef, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import * as Icon from "react-bootstrap-icons";
import PriorityIcon from "svg/icons/priority.svg";
import type { PriorityNumberType } from "rpc/transmission";
Expand All @@ -33,6 +33,9 @@ import { useHotkeysContext } from "hotkeys";
import { useHotkeys } from "@mantine/hooks";
import { modKeyString } from "trutil";
import { useServerSelectedTorrents } from "rpc/torrent";
import { ConfigContext } from "config";

const { saveJsonFile, loadJsonFile } = await import(/* webpackChunkName: "taurishim" */"taurishim");

interface ToolbarButtonProps extends React.PropsWithChildren<React.ComponentPropsWithRef<"button">> {
depressed?: boolean,
Expand Down Expand Up @@ -167,6 +170,8 @@ function useButtonHandlers(
}

function Toolbar(props: ToolbarProps) {
const config = useContext(ConfigContext);

const debouncedSetSearchTerms = useMemo(
() => debounce(props.setSearchTerms, 500, { trailing: true, leading: false }),
[props.setSearchTerms]);
Expand Down Expand Up @@ -208,6 +213,30 @@ function Toolbar(props: ToolbarProps) {
["mod + I", props.toggleDetailsPanel],
]);

const onSettingsExport = useCallback(() => {
void saveJsonFile(config.getExportedInterfaceSettings(), "trguing-interface.json");
}, [config]);

const onSettingsImport = useCallback(async () => {
try {
const settings = await loadJsonFile();
await config.tryMergeInterfaceSettings(JSON.parse(settings));
window.location.reload();
} catch (e) {
let msg = "";
if (typeof e === "string") {
msg = e;
} else if (e instanceof Error) {
msg = e.message;
}
notifications.show({
title: "Error importing settings",
message: msg,
color: "red",
});
}
}, [config]);

return (
<Flex w="100%" align="stretch">
<Button.Group mx="sm">
Expand Down Expand Up @@ -329,6 +358,14 @@ function Toolbar(props: ToolbarProps) {
onClick={props.toggleDetailsPanel} rightSection={<Kbd>{`${modKeyString()} I`}</Kbd>}>
Toggle details
</Menu.Item>
<Menu.Divider />
<Menu.Label>Interface settings</Menu.Label>
<Menu.Item onClick={onSettingsExport}>
Export
</Menu.Item>
<Menu.Item onClick={() => { void onSettingsImport(); }}>
Import
</Menu.Item>
</Menu.Dropdown>
</Menu>

Expand Down
36 changes: 35 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export interface SortByConfig {
}

interface TableSettings {
columns: string[],
columnVisibility: Record<string, boolean>,
columnOrder: string[],
columnSizes: Record<string, number>,
Expand Down Expand Up @@ -165,6 +164,7 @@ interface Settings {
styleOverrides: StyleOverrides,
progressbarStyle: ProgressbarStyleOption,
},
configVersion: number,
}

const DefaultColumnVisibility: Partial<Record<TableName, VisibilityState>> = {
Expand Down Expand Up @@ -286,6 +286,11 @@ const DefaultSettings: Settings = {
},
progressbarStyle: "animated",
},
// This field is used to verify config struct compatibility when importing settings
// Bump this only when incompatible changes are made that cannot be imported into older
// version.
// 1 is used in v1.4 and later
configVersion: 1,
};

export class Config {
Expand Down Expand Up @@ -429,6 +434,35 @@ export class Config {
if (index >= 0) saveDirs.splice(index, 1);
return saveDirs;
}

getExportedInterfaceSettings(): string {
const settings = {
interface: this.values.interface,
meta: {
configType: "trguing interface settings",
configVersion: this.values.configVersion,
},
};
return JSON.stringify(settings, null, 4);
}

async tryMergeInterfaceSettings(obj: any) {
if (!Object.prototype.hasOwnProperty.call(obj, "meta") ||
!Object.prototype.hasOwnProperty.call(obj, "interface") ||
!Object.prototype.hasOwnProperty.call(obj.meta, "configType") ||
!Object.prototype.hasOwnProperty.call(obj.meta, "configVersion") ||
obj.meta.configType !== "trguing interface settings") {
throw new Error("File does not appear to contain valid trguing interface settings");
}
if (obj.meta.configVersion > this.values.configVersion) {
throw new Error(
"This interface settings file was generated by a newer " +
"version of TrguiNG and can not be safely imported");
}
const merge = (await import(/* webpackChunkName: "lodash" */ "lodash-es/merge")).default;
merge(this.values.interface, obj.interface);
await this.save();
}
}

export const ConfigContext = React.createContext(new Config());
Expand Down
70 changes: 69 additions & 1 deletion src/taurishim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import type { OpenDialogOptions } from "@tauri-apps/api/dialog";
import type { OpenDialogOptions, SaveDialogOptions } from "@tauri-apps/api/dialog";
import type { EventCallback } from "@tauri-apps/api/event";
import type { CloseRequestedEvent, PhysicalPosition, PhysicalSize } from "@tauri-apps/api/window";

Expand Down Expand Up @@ -79,6 +79,11 @@ export const dialogOpen = TAURI
: async (options?: OpenDialogOptions) =>
await Promise.reject<string[] | string | null>(new Error("Running outside of tauri app"));

export const dialogSave = TAURI
? (await import(/* webpackMode: "lazy-once" */ "@tauri-apps/api/dialog")).save
: async (options?: SaveDialogOptions) =>
await Promise.reject<string | null>(new Error("Running outside of tauri app"));

export async function makeCreateTorrentView() {
if (WebviewWindow !== undefined) {
const webview = new WebviewWindow(`createtorrent-${Math.floor(Math.random() * 2 ** 30)}`, {
Expand Down Expand Up @@ -136,3 +141,66 @@ export function copyToClipboard(text: string) {
document.body.removeChild(textArea);
}
}

export async function saveJsonFile(contents: string, filename: string) {
if (fs !== undefined) {
dialogSave({
title: "Save interface settings",
defaultPath: filename,
filters: [{
name: "JSON",
extensions: ["json"],
}],
}).then((path) => {
if (path != null) {
void invoke("save_text_file", { contents, path });
}
}).catch(console.error);
} else {
const blob = new Blob([contents], { type: "application/json" });
const link = document.createElement("a");
const objurl = URL.createObjectURL(blob);
link.download = filename;
link.href = objurl;
link.click();
}
}

export async function loadJsonFile(): Promise<string> {
if (fs !== undefined) {
return await new Promise((resolve, reject) => {
dialogOpen({
title: "Select interface settings file",
filters: [{
name: "JSON",
extensions: ["json"],
}],
}).then((path) => {
if (path != null) {
invoke<string>("load_text_file", { path }).then(resolve).catch(reject);
}
}).catch(reject);
});
} else {
return await new Promise((resolve, reject) => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = () => {
const files = input.files;
if (files == null) reject(new Error("file not chosen"));
else {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error("Unable to read file"));
};
reader.readAsText(files[0], "UTF-8");
}
};
input.click();
});
}
}

0 comments on commit 5efef01

Please sign in to comment.