@ -0,0 +1,2 @@
|
||||
[build]
|
||||
#target = "wasm32-unknown-unknown"
|
@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
rm -rf ../target/release/bundle/osx/Postsack.app
|
||||
|
||||
# Build for x86 and ARM
|
||||
cargo build --release --target=aarch64-apple-darwin
|
||||
cargo build --release --target=x86_64-apple-darwin
|
||||
|
||||
# Combine into a fat binary
|
||||
|
||||
lipo -create ../target/aarch64-apple-darwin/release/postsack ../target/x86_64-apple-darwin/release/postsack -output postsack
|
||||
|
||||
# Perform Cargo bundle to create a macOS Bundle
|
||||
|
||||
cargo bundle --release
|
||||
|
||||
# Override bundle binary with the fat one
|
||||
# Also: We want to have `Postsack` capitalized on macOS, so we rename
|
||||
|
||||
rm ../target/release/bundle/osx/Postsack.app/Contents/MacOS/postsack
|
||||
|
||||
mv ./postsack ../target/release/bundle/osx/Postsack.app/Contents/MacOS/
|
||||
|
||||
# Tell the Info.plist or binary is capitalized
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable Postsack" "../target/release/bundle/osx/Postsack.app/Contents/Info.plist"
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 752 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 519 KiB |
@ -0,0 +1,10 @@
|
||||
use ps_database::Database;
|
||||
use ps_gui::{eframe, PostsackApp};
|
||||
|
||||
fn main() {
|
||||
#[cfg(debug_assertions)]
|
||||
ps_core::setup_tracing();
|
||||
|
||||
let options = eframe::NativeOptions::default();
|
||||
eframe::run_native(Box::new(PostsackApp::<Database>::new()), options);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use ps_core::{
|
||||
self,
|
||||
model::{self, Engine, Rect},
|
||||
Config, DatabaseLike, Field, Filter, FormatType, Importerlike, ValueField,
|
||||
Config, DatabaseLike, DatabaseQuery, Field, Filter, FormatType, Importerlike, ValueField,
|
||||
};
|
||||
use ps_database::Database;
|
||||
use ps_importer::mbox_importer;
|
@ -1,4 +1,4 @@
|
||||
use ps_core::{self, DatabaseLike, FormatType, Importerlike};
|
||||
use ps_core::{self, DatabaseLike, DatabaseQuery, FormatType, Importerlike};
|
||||
use ps_database::Database;
|
||||
use ps_importer;
|
||||
|
@ -0,0 +1,3 @@
|
||||
/target
|
||||
target
|
||||
.DS_Store
|
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "postsack-web"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "Provides a high level visual overview of swaths of email"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ps-gui = { path = "../ps-gui" }
|
||||
ps-core = { path = "../ps-core" }
|
||||
serde = { version = "1.0.131", features = ["derive"]}
|
||||
wasm-bindgen = "*"
|
||||
console_error_panic_hook = "0.1.7"
|
@ -0,0 +1,31 @@
|
||||
# Postsack Web
|
||||
|
||||
This is the WASM / Web version of Postsack. It uses fake email data to provide a web demo
|
||||
so that interested parties can try out Postsack native / the app without having to install
|
||||
it on their device.
|
||||
|
||||
## Building Postsack Web
|
||||
|
||||
First, you need to make sure all dependencies are installed:
|
||||
|
||||
``` sh
|
||||
cd postsack-web
|
||||
./setup_web.sh
|
||||
```
|
||||
|
||||
Once this is done, building can be performed with a single script:
|
||||
|
||||
``` sh
|
||||
./build_web.sh
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
In order to simplify testing, `build_web.sh` will launch a browser on `localhost:8080`.
|
||||
By default, `setup_web.sh` will install the `basic-http-server` so that you can run it
|
||||
in the `web_demo` folder prior to running `build-web.sh`:
|
||||
|
||||
``` sh
|
||||
cd web_demo
|
||||
basic-http-server -a 127.0.0.1:8080 .
|
||||
```
|
@ -0,0 +1,13 @@
|
||||
# Generate Data
|
||||
|
||||
This folder contains generated fake data from [https://generatedata.com/generator](https://generatedata.com/generator).
|
||||
|
||||
This data is used to generate `../src/generated.rs` so that the WASM build is compiled with data to be
|
||||
used in the web demo.
|
||||
|
||||
`generated.rs` can be rebuild via the following command:
|
||||
|
||||
``` sh
|
||||
cd fake_data # make sure you're in this folder
|
||||
python ./generate.py
|
||||
```
|
@ -0,0 +1,95 @@
|
||||
# parse all the json files in this folder and generate a rust file
|
||||
# containing generated data
|
||||
|
||||
import glob
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
|
||||
entries = []
|
||||
|
||||
output_rust_file = "../src/generated.rs"
|
||||
|
||||
# Coalesce data
|
||||
for json_file in glob.glob('*.json'):
|
||||
parsed = json.load(open(json_file, "r"))
|
||||
entries.extend(parsed)
|
||||
|
||||
# For each entry, generate a struct
|
||||
|
||||
to_address = "john@doe.com"
|
||||
to_name = ""
|
||||
|
||||
struct_template = """Entry { %(fields)s }"""
|
||||
|
||||
output_template = """
|
||||
use super::database::Entry;
|
||||
pub const ENTRIES: [Entry; %(amount)s] = [
|
||||
%(content)s
|
||||
];
|
||||
"""
|
||||
|
||||
# To generate some more data, we keep some email addresses to
|
||||
# generate 10-12 additional emails with that address afterwards
|
||||
additional_emails = []
|
||||
|
||||
generated_entries = []
|
||||
|
||||
def fields_from_entry(entry):
|
||||
k = {}
|
||||
k["sender_name"] = entry["name"]
|
||||
email = entry["email"].split("@")
|
||||
k["sender_domain"] = email[1]
|
||||
k["sender_local_part"] = email[0]
|
||||
date = entry["date"].split(",")
|
||||
(k["year"], k["month"], k["day"]) = (int(date[0]), int(date[1]), int(date[2]))
|
||||
k["timestamp"] = int(entry["time"])
|
||||
k["is_reply"] = True if entry["reply"] == 1 else False
|
||||
k["is_send"] = True if entry["send"] == 1 else False
|
||||
k["subject"] = entry["subject"]
|
||||
k["to_address"] = to_address
|
||||
k["to_name"] = to_name
|
||||
return k
|
||||
|
||||
def fields_to_string(k):
|
||||
fields = []
|
||||
for key in k:
|
||||
value = k[key]
|
||||
if type(value) == type(0):
|
||||
fields.append("%s: %s" % (key, value))
|
||||
elif type(value) == type(True):
|
||||
fields.append("%s: %s" % (key, "true" if value == True else "false"))
|
||||
elif type(value) == type(""):
|
||||
fields.append("%s: \"%s\"" % (key, value))
|
||||
elif type(value) == type(u""):
|
||||
fields.append("%s: \"%s\"" % (key, value))
|
||||
else:
|
||||
print(value, type(value))
|
||||
sys.exit(0)
|
||||
return ", ".join(fields)
|
||||
|
||||
# first run over the emails
|
||||
for entry in entries:
|
||||
k = fields_from_entry(entry)
|
||||
|
||||
# Generate additional mails
|
||||
if random.uniform(0.0, 1.0) > 0.7:
|
||||
for _ in range(0, random.randint(5, 50)):
|
||||
additional_emails.append((entry["email"], entry["name"]))
|
||||
|
||||
joined_fields = fields_to_string(k)
|
||||
generated_entries.append(struct_template % { "fields": joined_fields })
|
||||
|
||||
# second run over the email to generate additional entries with the same
|
||||
# email address so we have some clusters
|
||||
for (entry, (email, name)) in zip(entries, additional_emails):
|
||||
entry["email"] = email
|
||||
entry["name"] = name
|
||||
k = fields_from_entry(entry)
|
||||
joined_fields = fields_to_string(k)
|
||||
generated_entries.append(struct_template % { "fields": joined_fields })
|
||||
|
||||
writer = open(output_rust_file, "w")
|
||||
entries_string = ",\n".join(generated_entries)
|
||||
writer.write(output_template % { "content": entries_string, "amount": len(generated_entries) })
|
||||
writer.close()
|
@ -0,0 +1,341 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
use std::thread::JoinHandle;
|
||||
use std::{ops::Range, path::Path};
|
||||
|
||||
use ps_core::{
|
||||
crossbeam_channel::Sender,
|
||||
eyre::{bail, Result},
|
||||
Config, DBMessage, DatabaseLike, DatabaseQuery, Field, Filter, Query, QueryResult, Value,
|
||||
ValueField,
|
||||
};
|
||||
use ps_core::{OtherQuery, QueryRow};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Entry {
|
||||
pub sender_domain: &'static str,
|
||||
pub sender_local_part: &'static str,
|
||||
pub sender_name: &'static str,
|
||||
pub year: usize,
|
||||
pub month: usize,
|
||||
pub day: usize,
|
||||
pub timestamp: usize,
|
||||
pub subject: &'static str,
|
||||
pub to_name: &'static str,
|
||||
pub to_address: &'static str,
|
||||
pub is_reply: bool,
|
||||
pub is_send: bool,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn value(&self, field: &Field) -> Value {
|
||||
match field {
|
||||
Field::Path => Value::String("".to_string()),
|
||||
Field::SenderDomain => Value::String(self.sender_domain.to_string()),
|
||||
Field::SenderLocalPart => Value::String(self.sender_local_part.to_string()),
|
||||
Field::SenderName => Value::String(self.sender_name.to_string()),
|
||||
Field::Subject => Value::String(self.subject.to_string()),
|
||||
Field::ToName => Value::String(self.to_name.to_string()),
|
||||
Field::ToAddress => Value::String(self.to_address.to_string()),
|
||||
Field::ToGroup => Value::String("".to_string()),
|
||||
|
||||
Field::Year => Value::Number(self.year.into()),
|
||||
Field::Month => Value::Number(self.month.into()),
|
||||
Field::Day => Value::Number(self.day.into()),
|
||||
Field::Timestamp => Value::Number(self.timestamp.into()),
|
||||
|
||||
Field::IsReply => Value::Bool(self.is_reply),
|
||||
Field::IsSend => Value::Bool(self.is_send),
|
||||
|
||||
Field::MetaIsSeen => Value::Bool(false),
|
||||
Field::MetaTags => Value::Array(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_row(&self, fields: &[Field]) -> QueryRow {
|
||||
let mut row = QueryRow::new();
|
||||
for field in fields {
|
||||
let value = self.value(&field);
|
||||
let value_field = ValueField::new(&field, value);
|
||||
row.insert(*field, value_field);
|
||||
}
|
||||
row
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct HashedValue(Value);
|
||||
|
||||
impl Hash for HashedValue {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match &self.0 {
|
||||
Value::String(s) => s.hash(state),
|
||||
Value::Number(s) => s.hash(state),
|
||||
Value::Array(s) => {
|
||||
format!("{:?}", s).hash(state);
|
||||
}
|
||||
Value::Bool(s) => s.hash(state),
|
||||
_ => {
|
||||
format!("{:?}", &self.0).hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeDatabase;
|
||||
|
||||
impl FakeDatabase {
|
||||
pub fn total_item_count() -> usize {
|
||||
ENTRIES.len()
|
||||
}
|
||||
|
||||
fn query_normal(
|
||||
&self,
|
||||
fields: &Vec<Field>,
|
||||
filters: &Vec<Filter>,
|
||||
range: &Range<usize>,
|
||||
) -> Vec<QueryResult> {
|
||||
let entries = self.filtered(filters);
|
||||
let mut result = Vec::new();
|
||||
for entry in entries.skip(range.start).take(range.end) {
|
||||
result.push(QueryResult::Normal(entry.as_row(fields)));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn query_grouped(&self, filters: &Vec<Filter>, group_by: &Field) -> Vec<QueryResult> {
|
||||
let mut map = HashMap::<HashedValue, usize>::new();
|
||||
for entry in self
|
||||
.filtered(filters)
|
||||
.map(|e| HashedValue(e.value(group_by)))
|
||||
{
|
||||
let entry = map.entry(entry).or_insert(0);
|
||||
*entry += 1;
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
for (key, value) in map {
|
||||
result.push(QueryResult::Grouped {
|
||||
value: ValueField::new(group_by, key.0),
|
||||
count: value,
|
||||
})
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn query_other(&self, field: &Field) -> Vec<QueryResult> {
|
||||
let mut set = HashSet::<HashedValue>::new();
|
||||
for entry in &ENTRIES {
|
||||
let hashed_entry = HashedValue(entry.value(field));
|
||||
if !set.contains(&hashed_entry) {
|
||||
set.insert(hashed_entry);
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
for value in set {
|
||||
result.push(QueryResult::Other(ValueField::new(field, value.0)));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn filtered<'a>(&'a self, filters: &'a Vec<Filter>) -> impl Iterator<Item = &'a Entry> {
|
||||
ENTRIES.iter().filter(move |entry| {
|
||||
for filter in filters {
|
||||
// Go through all filters and escape early if they don't match
|
||||
match filter {
|
||||
Filter::Like(vf) => {
|
||||
let other = entry.value(vf.field());
|
||||
if vf.value() != &other {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::NotLike(vf) => {
|
||||
let other = entry.value(vf.field());
|
||||
if vf.value() == &other {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Contains(vf) => {
|
||||
let other = entry.value(vf.field());
|
||||
match (&other, vf.value()) {
|
||||
(Value::String(a), Value::String(b)) => {
|
||||
if !a.contains(b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let s1 = format!("{}", vf.value());
|
||||
let s2 = format!("{}", &other);
|
||||
if !s2.contains(&s1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Filter::Is(vf) => {
|
||||
let other = entry.value(vf.field());
|
||||
if vf.value() != &other {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FakeDatabase {
|
||||
fn clone(&self) -> Self {
|
||||
FakeDatabase
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabaseQuery for FakeDatabase {
|
||||
fn query(&self, query: &Query) -> Result<Vec<QueryResult>> {
|
||||
match query {
|
||||
Query::Normal {
|
||||
fields,
|
||||
filters,
|
||||
range,
|
||||
} => Ok(self.query_normal(fields, filters, range)),
|
||||
Query::Grouped { filters, group_by } => Ok(self.query_grouped(filters, group_by)),
|
||||
Query::Other {
|
||||
query: OtherQuery::All(q),
|
||||
} => Ok(self.query_other(q)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabaseLike for FakeDatabase {
|
||||
fn new(_path: impl AsRef<Path>) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(FakeDatabase {})
|
||||
}
|
||||
|
||||
fn config(_path: impl AsRef<Path>) -> Result<Config>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
bail!("Na")
|
||||
}
|
||||
|
||||
fn total_mails(&self) -> Result<usize> {
|
||||
Ok(ENTRIES.len())
|
||||
}
|
||||
|
||||
fn import(self) -> (Sender<DBMessage>, JoinHandle<Result<usize>>) {
|
||||
panic!()
|
||||
}
|
||||
fn save_config(&self, _config: Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use super::generated::ENTRIES;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const ENTRIES: [Entry; 7] = [
|
||||
Entry {
|
||||
sender_local_part: "tellus.non.magna",
|
||||
is_send: true,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Sybill Fleming",
|
||||
timestamp: 1625731134,
|
||||
month: 12,
|
||||
to_name: "",
|
||||
is_reply: false,
|
||||
year: 2013,
|
||||
sender_domain: "protonmail.edu",
|
||||
day: 28,
|
||||
subject: "libero et tristique pellentesque, tellus sem mollis dui,",
|
||||
},
|
||||
Entry {
|
||||
sender_local_part: "mauris.sapien",
|
||||
is_send: true,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Ignatius Reed",
|
||||
timestamp: 1645571678,
|
||||
month: 10,
|
||||
to_name: "",
|
||||
is_reply: true,
|
||||
year: 2020,
|
||||
sender_domain: "icloud.com",
|
||||
day: 26,
|
||||
subject: "nisi magna sed dui. Fusce aliquam,",
|
||||
},
|
||||
Entry {
|
||||
sender_local_part: "magna.nam",
|
||||
is_send: false,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Geraldine Gay",
|
||||
timestamp: 1631684202,
|
||||
month: 8,
|
||||
to_name: "",
|
||||
is_reply: true,
|
||||
year: 2016,
|
||||
sender_domain: "aol.org",
|
||||
day: 18,
|
||||
subject: "semper auctor. Mauris vel turpis. Aliquam adipiscing",
|
||||
},
|
||||
Entry {
|
||||
sender_local_part: "tortor",
|
||||
is_send: true,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Colt Clark",
|
||||
timestamp: 1640866204,
|
||||
month: 4,
|
||||
to_name: "",
|
||||
is_reply: true,
|
||||
year: 2012,
|
||||
sender_domain: "aol.ca",
|
||||
day: 2,
|
||||
subject: "hendrerit id, ante. Nunc mauris sapien, cursus",
|
||||
},
|
||||
Entry {
|
||||
sender_local_part: "urna.convallis.erat",
|
||||
is_send: true,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Joy Clark",
|
||||
timestamp: 1646836804,
|
||||
month: 2,
|
||||
to_name: "",
|
||||
is_reply: true,
|
||||
year: 2020,
|
||||
sender_domain: "protonmail.ca",
|
||||
day: 10,
|
||||
subject: "dui nec urna suscipit nonummy. Fusce fermentum fermentum arcu. Vestibulum",
|
||||
},
|
||||
Entry {
|
||||
sender_local_part: "amet.luctus",
|
||||
is_send: false,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Ray Bowers",
|
||||
timestamp: 1609958850,
|
||||
month: 6,
|
||||
to_name: "",
|
||||
is_reply: false,
|
||||
year: 2015,
|
||||
sender_domain: "protonmail.org",
|
||||
day: 30,
|
||||
subject: "turpis egestas. Aliquam fringilla cursus",
|
||||
},
|
||||
Entry {
|
||||
sender_local_part: "vehicula.et",
|
||||
is_send: true,
|
||||
to_address: "john@doe.com",
|
||||
sender_name: "Maris Shaw",
|
||||
timestamp: 1612463990,
|
||||
month: 10,
|
||||
to_name: "",
|
||||
is_reply: false,
|
||||
year: 2018,
|
||||
sender_domain: "hotmail.ca",
|
||||
day: 30,
|
||||
subject: "molestie orci tincidunt",
|
||||
},
|
||||
];
|
@ -0,0 +1,31 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use ps_core::{Config, FormatType};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use ps_gui::{eframe, PostsackApp};
|
||||
|
||||
mod database;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod generated;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use console_error_panic_hook;
|
||||
|
||||
/// Call this once from the HTML.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
|
||||
use database::FakeDatabase;
|
||||
|
||||
let format = FormatType::AppleMail;
|
||||
let config = Config::new(None, "", vec!["test@gmail.com".to_owned()], format).unwrap();
|
||||
|
||||
let app = PostsackApp::<database::FakeDatabase>::new(config, FakeDatabase::total_item_count());
|
||||
eframe::start_web(canvas_id, Box::new(app))
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<!-- Disable zooming: -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<head>
|
||||
<title>Postsack Web</title>
|
||||
<style>
|
||||
html {
|
||||
/* Remove touch delay: */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Light mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
/* Dark mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #404040;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allow canvas to fill entire web page: */
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Position canvas in center-top: */
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize this dynamically -->
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<script>
|
||||
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
|
||||
// `WebAssembly.instantiateStreaming` to instantiate the wasm module,
|
||||
// but this doesn't work with `file://` urls. This example is frequently
|
||||
// viewed by simply opening `index.html` in a browser (with a `file://`
|
||||
// url), so it would fail if we were to call this function!
|
||||
//
|
||||
// Work around this for now by deleting the function to ensure that the
|
||||
// `no_modules.js` script doesn't have access to it. You won't need this
|
||||
// hack when deploying over HTTP.
|
||||
delete WebAssembly.instantiateStreaming;
|
||||
</script>
|
||||
|
||||
<!-- this is the JS generated by the `wasm-bindgen` CLI tool -->
|
||||
<script src="postsack_web.js"></script>
|
||||
|
||||
<script>
|
||||
// We'll defer our execution until the wasm is ready to go.
|
||||
// Here we tell bindgen the path to the wasm file so it can start
|
||||
// initialization and return to us a promise when it's done.
|
||||
wasm_bindgen("./postsack_web_bg.wasm")
|
||||
.then(on_wasm_loaded)
|
||||
.catch(console.error);
|
||||
|
||||
function on_wasm_loaded() {
|
||||
// This call installs a bunch of callbacks and then returns.
|
||||
console.log("loaded wasm, starting egui app");
|
||||
wasm_bindgen.start("the_canvas_id");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
@ -1,6 +0,0 @@
|
||||
fn main() {
|
||||
#[cfg(debug_assertions)]
|
||||
ps_core::setup_tracing();
|
||||
|
||||
ps_gui::run_ui();
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
rm -rf target/release/bundle/osx/Postsack.app
|
||||
|
||||
# Build for x86 and ARM
|
||||
cargo build --release --target=aarch64-apple-darwin
|
||||
cargo build --release --target=x86_64-apple-darwin
|
||||
|
||||
# Combine into a fat binary
|
||||
|
||||
lipo -create target/aarch64-apple-darwin/release/postsack target/x86_64-apple-darwin/release/postsack -output postsack
|
||||
|
||||
# Perform Cargo bundle to create a macOS Bundle
|
||||
|
||||
cargo bundle --release
|
||||
|
||||
# Override bundle binary with the fat one
|
||||
# Also: We want to have `Postsack` capitalized on macOS, so we rename
|
||||
|
||||
rm target/release/bundle/osx/Postsack.app/Contents/MacOS/postsack
|
||||
|
||||
mv ./postsack target/release/bundle/osx/Postsack.app/Contents/MacOS/
|
||||
|
||||
# Tell the Info.plist or binary is capitalized
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable Postsack" "target/release/bundle/osx/Postsack.app/Contents/Info.plist"
|
||||
|
@ -0,0 +1,48 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use eframe::egui::{self, Color32};
|
||||
use eyre::Result;
|
||||
|
||||
use super::{PlatformColors, Theme};
|
||||
|
||||
pub fn platform_colors(theme: Theme) -> PlatformColors {
|
||||
// From Google images, Windows 11
|
||||
match theme {
|
||||
Theme::Light => PlatformColors {
|
||||
is_light: true,
|
||||
animation_background: Color32::from_rgb(248, 246, 249),
|
||||
window_background: Color32::from_rgb(241, 243, 246),
|
||||
content_background: Color32::from_rgb(251, 251, 253),
|
||||
text_primary: Color32::from_gray(0),
|
||||
text_secondary: Color32::from_gray(30),
|
||||
line1: Color32::from_gray(0),
|
||||
line2: Color32::from_gray(30),
|
||||
line3: Color32::from_gray(60),
|
||||
line4: Color32::from_gray(90),
|
||||
},
|
||||
Theme::Dark => PlatformColors {
|
||||
is_light: false,
|
||||
animation_background: Color32::from_gray(60),
|
||||
window_background: Color32::from_rgb(32, 32, 32),
|
||||
content_background: Color32::from_rgb(34, 32, 40),
|
||||
text_primary: Color32::from_gray(255),
|
||||
text_secondary: Color32::from_gray(200),
|
||||
line1: Color32::from_gray(255),
|
||||
line2: Color32::from_gray(210),
|
||||
line3: Color32::from_gray(190),
|
||||
line4: Color32::from_gray(120),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// This is called from `App::setup`
|
||||
pub fn setup(ctx: &egui::CtxRef) {}
|
||||
|
||||
/// This is called once from `App::update` on the first run.
|
||||
pub fn initial_update(ctx: &egui::CtxRef) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn navigation_button(title: &str) -> egui::Button {
|
||||
egui::Button::new(title)
|
||||
}
|