servers list
This commit is contained in:
Generated
+1
@@ -5973,6 +5973,7 @@ dependencies = [
|
|||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-specta",
|
"tauri-specta",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
"windows 0.58.0",
|
"windows 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "migra
|
|||||||
tauri-specta = { version = "2.0.0-rc.21", features = ["derive", "typescript"] }
|
tauri-specta = { version = "2.0.0-rc.21", features = ["derive", "typescript"] }
|
||||||
specta = "2.0.0-rc.21"
|
specta = "2.0.0-rc.21"
|
||||||
specta-typescript = { version = "0.0.9" }
|
specta-typescript = { version = "0.0.9" }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE remotes (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
port INTEGER CHECK (port IS NULL OR port BETWEEN 0 AND 65535)
|
||||||
|
);
|
||||||
+10
-1
@@ -2,6 +2,7 @@ mod cursor;
|
|||||||
mod db;
|
mod db;
|
||||||
mod friends;
|
mod friends;
|
||||||
mod keypair;
|
mod keypair;
|
||||||
|
mod remotes;
|
||||||
mod ufa;
|
mod ufa;
|
||||||
mod windowing;
|
mod windowing;
|
||||||
|
|
||||||
@@ -24,9 +25,17 @@ pub fn run() {
|
|||||||
friends::list_friends,
|
friends::list_friends,
|
||||||
friends::get_friend,
|
friends::get_friend,
|
||||||
friends::delete_friend,
|
friends::delete_friend,
|
||||||
|
remotes::create_remote,
|
||||||
|
remotes::list_remotes,
|
||||||
|
remotes::get_remote,
|
||||||
|
remotes::update_remote,
|
||||||
|
remotes::delete_remote,
|
||||||
keypair::get_public_key,
|
keypair::get_public_key,
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![friends::FriendsChanged]);
|
.events(tauri_specta::collect_events![
|
||||||
|
friends::FriendsChanged,
|
||||||
|
remotes::RemotesChanged,
|
||||||
|
]);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
specta_builder
|
specta_builder
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use crate::db::{self, AppDatabase};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use tauri::{AppHandle, State};
|
||||||
|
use tauri_specta::Event;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type, sqlx::FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Remote {
|
||||||
|
pub id: String,
|
||||||
|
pub address: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RemoteInput {
|
||||||
|
pub address: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RemotesChanged {
|
||||||
|
pub remotes: Vec<Remote>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn all(database: &AppDatabase) -> Result<Vec<Remote>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Remote>(
|
||||||
|
"SELECT id, address, name, port FROM remotes \
|
||||||
|
ORDER BY COALESCE(name, address) COLLATE NOCASE, id",
|
||||||
|
)
|
||||||
|
.fetch_all(database.pool())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(database: &AppDatabase, id: &str) -> Result<Option<Remote>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Remote>("SELECT id, address, name, port FROM remotes WHERE id = ?1")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(database.pool())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert(database: &AppDatabase, input: RemoteInput) -> Result<Remote, sqlx::Error> {
|
||||||
|
let remote = Remote {
|
||||||
|
id: Uuid::new_v4().to_string(),
|
||||||
|
address: input.address,
|
||||||
|
name: input.name,
|
||||||
|
port: input.port,
|
||||||
|
};
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO remotes (id, address, name, port) VALUES (?1, ?2, ?3, ?4)")
|
||||||
|
.bind(&remote.id)
|
||||||
|
.bind(&remote.address)
|
||||||
|
.bind(&remote.name)
|
||||||
|
.bind(remote.port)
|
||||||
|
.execute(database.pool())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
database: &AppDatabase,
|
||||||
|
id: &str,
|
||||||
|
input: RemoteInput,
|
||||||
|
) -> Result<Option<Remote>, sqlx::Error> {
|
||||||
|
let result = sqlx::query("UPDATE remotes SET address = ?1, name = ?2, port = ?3 WHERE id = ?4")
|
||||||
|
.bind(&input.address)
|
||||||
|
.bind(&input.name)
|
||||||
|
.bind(input.port)
|
||||||
|
.bind(id)
|
||||||
|
.execute(database.pool())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(Remote {
|
||||||
|
id: id.to_owned(),
|
||||||
|
address: input.address,
|
||||||
|
name: input.name,
|
||||||
|
port: input.port,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(database: &AppDatabase, id: &str) -> Result<bool, sqlx::Error> {
|
||||||
|
let result = sqlx::query("DELETE FROM remotes WHERE id = ?1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(database.pool())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result.rows_affected() > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emit_changed(handle: &AppHandle, database: &AppDatabase) -> Result<(), String> {
|
||||||
|
RemotesChanged {
|
||||||
|
remotes: all(database).await.map_err(db::command_error)?,
|
||||||
|
}
|
||||||
|
.emit(handle)
|
||||||
|
.map_err(db::command_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn create_remote(
|
||||||
|
handle: AppHandle,
|
||||||
|
database: State<'_, AppDatabase>,
|
||||||
|
remote: RemoteInput,
|
||||||
|
) -> Result<Remote, String> {
|
||||||
|
let remote = insert(&database, remote).await.map_err(db::command_error)?;
|
||||||
|
emit_changed(&handle, &database).await?;
|
||||||
|
Ok(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn list_remotes(
|
||||||
|
handle: AppHandle,
|
||||||
|
database: State<'_, AppDatabase>,
|
||||||
|
) -> Result<Vec<Remote>, String> {
|
||||||
|
let remotes = all(&database).await.map_err(db::command_error)?;
|
||||||
|
|
||||||
|
RemotesChanged {
|
||||||
|
remotes: remotes.clone(),
|
||||||
|
}
|
||||||
|
.emit(&handle)
|
||||||
|
.map_err(db::command_error)?;
|
||||||
|
|
||||||
|
Ok(remotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_remote(
|
||||||
|
database: State<'_, AppDatabase>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Option<Remote>, String> {
|
||||||
|
get(&database, &id).await.map_err(db::command_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn update_remote(
|
||||||
|
handle: AppHandle,
|
||||||
|
database: State<'_, AppDatabase>,
|
||||||
|
id: String,
|
||||||
|
remote: RemoteInput,
|
||||||
|
) -> Result<Option<Remote>, String> {
|
||||||
|
let remote = update(&database, &id, remote)
|
||||||
|
.await
|
||||||
|
.map_err(db::command_error)?;
|
||||||
|
|
||||||
|
if remote.is_some() {
|
||||||
|
emit_changed(&handle, &database).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn delete_remote(
|
||||||
|
handle: AppHandle,
|
||||||
|
database: State<'_, AppDatabase>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let changed = delete(&database, &id).await.map_err(db::command_error)?;
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
emit_changed(&handle, &database).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
async fn database() -> AppDatabase {
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("connect to in-memory SQLite");
|
||||||
|
sqlx::migrate!("./migrations")
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.expect("run database migrations");
|
||||||
|
AppDatabase::new(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn insert_generates_a_uuid_v4_and_preserves_optional_fields() {
|
||||||
|
let database = database().await;
|
||||||
|
let remote = insert(
|
||||||
|
&database,
|
||||||
|
RemoteInput {
|
||||||
|
address: "play.example.com".to_owned(),
|
||||||
|
name: None,
|
||||||
|
port: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("insert remote");
|
||||||
|
|
||||||
|
let id = Uuid::parse_str(&remote.id).expect("parse generated UUID");
|
||||||
|
assert_eq!(id.get_version_num(), 4);
|
||||||
|
assert_eq!(remote.address, "play.example.com");
|
||||||
|
assert_eq!(remote.name, None);
|
||||||
|
assert_eq!(remote.port, None);
|
||||||
|
assert_eq!(get(&database, &remote.id).await.unwrap(), Some(remote));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_and_delete_cover_the_remote_lifecycle() {
|
||||||
|
let database = database().await;
|
||||||
|
let created = insert(
|
||||||
|
&database,
|
||||||
|
RemoteInput {
|
||||||
|
address: "old.example.com".to_owned(),
|
||||||
|
name: Some("Old name".to_owned()),
|
||||||
|
port: Some(25565),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("insert remote");
|
||||||
|
|
||||||
|
let updated = update(
|
||||||
|
&database,
|
||||||
|
&created.id,
|
||||||
|
RemoteInput {
|
||||||
|
address: "new.example.com".to_owned(),
|
||||||
|
name: Some("New name".to_owned()),
|
||||||
|
port: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("update remote")
|
||||||
|
.expect("remote exists");
|
||||||
|
|
||||||
|
assert_eq!(updated.id, created.id);
|
||||||
|
assert_eq!(updated.address, "new.example.com");
|
||||||
|
assert_eq!(updated.name.as_deref(), Some("New name"));
|
||||||
|
assert_eq!(updated.port, None);
|
||||||
|
assert_eq!(all(&database).await.unwrap(), vec![updated.clone()]);
|
||||||
|
assert!(delete(&database, &updated.id).await.unwrap());
|
||||||
|
assert!(!delete(&database, &updated.id).await.unwrap());
|
||||||
|
assert_eq!(get(&database, &updated.id).await.unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_returns_none_for_an_unknown_remote() {
|
||||||
|
let database = database().await;
|
||||||
|
|
||||||
|
let updated = update(
|
||||||
|
&database,
|
||||||
|
"missing",
|
||||||
|
RemoteInput {
|
||||||
|
address: "example.com".to_owned(),
|
||||||
|
name: None,
|
||||||
|
port: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("update query succeeds");
|
||||||
|
|
||||||
|
assert_eq!(updated, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-2
@@ -17,6 +17,21 @@ async getFriend(id: string) : Promise<Friend | null> {
|
|||||||
async deleteFriend(id: string) : Promise<boolean> {
|
async deleteFriend(id: string) : Promise<boolean> {
|
||||||
return await TAURI_INVOKE("delete_friend", { id });
|
return await TAURI_INVOKE("delete_friend", { id });
|
||||||
},
|
},
|
||||||
|
async createRemote(remote: RemoteInput) : Promise<Remote> {
|
||||||
|
return await TAURI_INVOKE("create_remote", { remote });
|
||||||
|
},
|
||||||
|
async listRemotes() : Promise<Remote[]> {
|
||||||
|
return await TAURI_INVOKE("list_remotes");
|
||||||
|
},
|
||||||
|
async getRemote(id: string) : Promise<Remote | null> {
|
||||||
|
return await TAURI_INVOKE("get_remote", { id });
|
||||||
|
},
|
||||||
|
async updateRemote(id: string, remote: RemoteInput) : Promise<Remote | null> {
|
||||||
|
return await TAURI_INVOKE("update_remote", { id, remote });
|
||||||
|
},
|
||||||
|
async deleteRemote(id: string) : Promise<boolean> {
|
||||||
|
return await TAURI_INVOKE("delete_remote", { id });
|
||||||
|
},
|
||||||
async getPublicKey() : Promise<string> {
|
async getPublicKey() : Promise<string> {
|
||||||
return await TAURI_INVOKE("get_public_key");
|
return await TAURI_INVOKE("get_public_key");
|
||||||
}
|
}
|
||||||
@@ -26,9 +41,11 @@ async getPublicKey() : Promise<string> {
|
|||||||
|
|
||||||
|
|
||||||
export const events = __makeEvents__<{
|
export const events = __makeEvents__<{
|
||||||
friendsChanged: FriendsChanged
|
friendsChanged: FriendsChanged,
|
||||||
|
remotesChanged: RemotesChanged
|
||||||
}>({
|
}>({
|
||||||
friendsChanged: "friends-changed"
|
friendsChanged: "friends-changed",
|
||||||
|
remotesChanged: "remotes-changed"
|
||||||
})
|
})
|
||||||
|
|
||||||
/** user-defined constants **/
|
/** user-defined constants **/
|
||||||
@@ -39,6 +56,9 @@ friendsChanged: "friends-changed"
|
|||||||
|
|
||||||
export type Friend = { id: string; displayName: string }
|
export type Friend = { id: string; displayName: string }
|
||||||
export type FriendsChanged = { friends: Friend[] }
|
export type FriendsChanged = { friends: Friend[] }
|
||||||
|
export type Remote = { id: string; address: string; name: string | null; port: number | null }
|
||||||
|
export type RemoteInput = { address: string; name: string | null; port: number | null }
|
||||||
|
export type RemotesChanged = { remotes: Remote[] }
|
||||||
|
|
||||||
/** tauri-specta globals **/
|
/** tauri-specta globals **/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { commands, events, type Remote, type RemoteInput } from "$lib/bindings";
|
||||||
|
|
||||||
|
let remotes: Remote[] = [];
|
||||||
|
let editingId: string | null = null;
|
||||||
|
let error = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const unlisten = events.remotesChanged.listen((event) => {
|
||||||
|
remotes = event.payload.remotes;
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.listRemotes().catch((err) => {
|
||||||
|
error = String(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((stop) => stop());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function remoteInput(form: HTMLFormElement): RemoteInput {
|
||||||
|
const data = new FormData(form);
|
||||||
|
const name = String(data.get("name") ?? "").trim();
|
||||||
|
const port = String(data.get("port") ?? "").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: String(data.get("address") ?? "").trim(),
|
||||||
|
name: name || null,
|
||||||
|
port: port ? Number(port) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRemote(event: SubmitEvent) {
|
||||||
|
const form = event.currentTarget as HTMLFormElement;
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await commands.createRemote(remoteInput(form));
|
||||||
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRemote(event: SubmitEvent, id: string) {
|
||||||
|
const form = event.currentTarget as HTMLFormElement;
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await commands.updateRemote(id, remoteInput(form));
|
||||||
|
editingId = null;
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRemote(id: string) {
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await commands.deleteRemote(id);
|
||||||
|
if (editingId === id) {
|
||||||
|
editingId = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>Remotes</h1>
|
||||||
|
|
||||||
|
<form onsubmit={createRemote}>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Address
|
||||||
|
<input name="address" required />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Name (optional)
|
||||||
|
<input name="name" />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Port (optional)
|
||||||
|
<input name="port" type="number" min="1" max="65535" inputmode="numeric" />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="submit">Create remote</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p aria-live="polite">{error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if remotes.length === 0}
|
||||||
|
<p>No remotes saved.</p>
|
||||||
|
{:else}
|
||||||
|
<ul>
|
||||||
|
{#each remotes as remote (remote.id)}
|
||||||
|
<li>
|
||||||
|
{#if editingId === remote.id}
|
||||||
|
<form onsubmit={(event) => updateRemote(event, remote.id)}>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Address
|
||||||
|
<input name="address" value={remote.address} required />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Name (optional)
|
||||||
|
<input name="name" value={remote.name ?? ""} />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Port (optional)
|
||||||
|
<input
|
||||||
|
name="port"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
inputmode="numeric"
|
||||||
|
value={remote.port ?? ""}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
<button type="button" onclick={() => (editingId = null)}>Cancel</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
<strong>{remote.name ?? remote.address}</strong>
|
||||||
|
{#if remote.name}
|
||||||
|
<span> — {remote.address}</span>
|
||||||
|
{/if}
|
||||||
|
{#if remote.port !== null}
|
||||||
|
<span>:{remote.port}</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<p>ID: {remote.id}</p>
|
||||||
|
<button type="button" onclick={() => (editingId = remote.id)}>Edit</button>
|
||||||
|
<button type="button" onclick={() => deleteRemote(remote.id)}>Delete</button>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import FriendsList from "$lib/components/friends-list.svelte";
|
import FriendsList from "$lib/components/friends-list.svelte";
|
||||||
|
import RemotesList from "$lib/components/remotes-list.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div>
|
<div>
|
||||||
<FriendsList />
|
<FriendsList />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<RemotesList />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user