Skip to content

Commit

Permalink
Merge pull request #181 from CapSoftware/s3-config
Browse files Browse the repository at this point in the history
feat: Cap Apps + Custom S3 Config functionality
  • Loading branch information
richiemcilroy authored Nov 23, 2024
2 parents 018d2c3 + 939c160 commit 11079ae
Show file tree
Hide file tree
Showing 44 changed files with 1,672 additions and 361 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ NEXT_PUBLIC_LOCAL_MODE=false
# which will automatically be created on pnpm dev.
# It is used for local development only.
# For production, use the DATABASE_URL from your DB provider. Must include https for production use, mysql:// for local.
# DATABASE_ENCRYPTION_KEY is used to encrypt sensitive data in the database (like S3 credentials). Not required for local development.
DATABASE_URL=mysql://root:@localhost:3306/planetscale
DATABASE_ENCRYPTION_KEY=

# This is the secret for the NextAuth.js authentication library.
# It is used for local development only.
Expand Down Expand Up @@ -74,4 +76,4 @@ CAP_DESKTOP_SENTRY_URL=https://efd3156d9c0a8a49bee3ee675bec80d8@o450685977152716

# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
8 changes: 5 additions & 3 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,7 +1500,10 @@ fn list_screenshots(app: AppHandle) -> Result<Vec<(String, PathBuf, RecordingMet
#[specta::specta]
async fn check_upgraded_and_update(app: AppHandle) -> Result<bool, String> {
if let Err(e) = AuthStore::fetch_and_update_plan(&app).await {
return Err(format!("Failed to update plan information: {}", e));
return Err(format!(
"Failed to update plan information. Try signing out and signing back in: {}",
e
));
}

let auth = AuthStore::get(&app).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -1536,12 +1539,11 @@ async fn delete_auth_open_signin(app: AppHandle) -> Result<(), String> {
if let Some(window) = CapWindowId::Camera.get(&app) {
window.close().ok();
}

if let Some(window) = CapWindowId::Main.get(&app) {
window.close().ok();
}

while CapWindowId::Main.get(&app).is_some() {
while CapWindowId::Main.get(&app).is_none() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}

Expand Down
54 changes: 53 additions & 1 deletion apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,59 @@ use tokio::task;
use crate::web_api::{self, ManagerExt};

use crate::UploadProgress;
use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use specta::Type;

#[derive(Deserialize, Serialize, Clone, Type)]
pub struct S3UploadMeta {
id: String,
user_id: String,
#[serde(default)]
aws_region: String,
#[serde(default, deserialize_with = "deserialize_empty_object_as_string")]
aws_bucket: String,
}

fn deserialize_empty_object_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrObject;

impl<'de> de::Visitor<'de> for StringOrObject {
type Value = String;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or empty object")
}

fn visit_str<E>(self, value: &str) -> Result<String, E>
where
E: de::Error,
{
Ok(value.to_string())
}

fn visit_string<E>(self, value: String) -> Result<String, E>
where
E: de::Error,
{
Ok(value)
}

fn visit_map<M>(self, _map: M) -> Result<String, M::Error>
where
M: de::MapAccess<'de>,
{
// Return empty string for empty objects
Ok(String::new())
}
}

deserializer.deserialize_any(StringOrObject)
}

impl S3UploadMeta {
pub fn id(&self) -> &str {
&self.id
Expand All @@ -48,6 +90,15 @@ impl S3UploadMeta {
aws_bucket,
}
}

pub fn ensure_defaults(&mut self) {
if self.aws_region.is_empty() {
self.aws_region = std::env::var("NEXT_PUBLIC_CAP_AWS_REGION").unwrap_or_default();
}
if self.aws_bucket.is_empty() {
self.aws_bucket = std::env::var("NEXT_PUBLIC_CAP_AWS_BUCKET").unwrap_or_default();
}
}
}

#[derive(serde::Serialize)]
Expand Down Expand Up @@ -456,13 +507,14 @@ pub async fn get_s3_config(
.await
.map_err(|e| format!("Failed to read response body: {}", e))?;

let config = serde_json::from_str::<S3UploadMeta>(&response_text).map_err(|e| {
let mut config = serde_json::from_str::<S3UploadMeta>(&response_text).map_err(|e| {
format!(
"Failed to deserialize response: {}. Response body: {}",
e, response_text
)
})?;

config.ensure_defaults();
Ok(config)
}

Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,12 @@ fn position_traffic_lights_impl(
let c_win = window.clone();
window
.run_on_main_thread(move || {
let ns_window = match c_win.ns_window() {
Ok(handle) => handle,
Err(_) => return,
};
position_window_controls(
UnsafeWindowHandle(
c_win
.ns_window()
.expect("Failed to get native window handle"),
),
UnsafeWindowHandle(ns_window),
&controls_inset.unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET),
);
})
Expand Down
39 changes: 38 additions & 1 deletion apps/desktop/src/routes/(window-chrome)/(main).tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export default function () {
},
}));

const [isUpgraded] = createResource(() => commands.checkUpgradedAndUpdate());

createAsync(() => getAuth());

createUpdateCheck();
Expand Down Expand Up @@ -118,8 +120,43 @@ export default function () {
return (
<div class="flex justify-center flex-col p-[1rem] gap-[0.75rem] text-[0.875rem] font-[400] bg-gray-50 h-full">
<div class="flex items-center justify-between pb-[0.25rem]">
<IconCapLogoFull class="w-[90px] h-auto" />
<div class="flex items-center space-x-1">
<IconCapLogoFull class="w-[90px] h-auto" />
<span
onClick={async () => {
if (!isUpgraded()) {
await commands.showWindow("Upgrade");
}
}}
class={`text-[0.625rem] ${
isUpgraded()
? "bg-blue-400 text-gray-50"
: "bg-gray-200 text-gray-400 cursor-pointer hover:bg-gray-300"
} rounded-lg px-1.5 py-0.5`}
>
{isUpgraded() ? "Pro" : "Free"}
</span>
</div>
<div class="flex items-center space-x-2">
<Tooltip.Root openDelay={0}>
<Tooltip.Trigger>
<button
type="button"
onClick={() =>
commands.showWindow({ Settings: { page: "apps" } })
}
>
<IconLucideLayoutGrid class="w-[1.25rem] h-[1.25rem] text-gray-400 hover:text-gray-500" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content class="z-50 px-2 py-1 text-xs text-gray-50 bg-gray-500 rounded shadow-lg animate-in fade-in duration-100">
Cap Apps
<Tooltip.Arrow class="fill-gray-500" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>

<Tooltip.Root openDelay={0}>
<Tooltip.Trigger>
<button
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/routes/(window-chrome)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export default function Settings(props: RouteSectionProps) {
name: "Previous Screenshots",
icon: IconLucideCamera,
},
{
href: "apps",
name: "Cap Apps",
icon: IconLucideLayoutGrid,
},
{
href: "feedback",
name: "Feedback",
Expand Down
60 changes: 60 additions & 0 deletions apps/desktop/src/routes/(window-chrome)/settings/apps/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Button } from "@cap/ui-solid";
import { useNavigate } from "@solidjs/router";
import { For, createResource } from "solid-js";
import { commands } from "~/utils/tauri";

export default function AppsTab() {
const navigate = useNavigate();
const [isUpgraded] = createResource(() => commands.checkUpgradedAndUpdate());

const apps = [
{
name: "S3 Config",
description:
"Connect your own S3 bucket. All new shareable link uploads will be uploaded here. Maintain complete ownership over your data.",
icon: IconLucideDatabase,
url: "/settings/apps/s3-config",
pro: true,
},
];

const handleAppClick = async (app: (typeof apps)[number]) => {
if (app.pro && !isUpgraded()) {
await commands.showWindow("Upgrade");
return;
}
navigate(app.url);
};

return (
<div class="p-4">
<For each={apps}>
{(app) => (
<div class="p-1.5 bg-white rounded-lg border border-gray-200">
<div class="flex justify-between items-center border-b border-gray-200 pb-2">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-gray-100">
<app.icon class="w-4 h-4 text-gray-500" />
</div>
<div class="flex flex-col gap-1">
<span class="text-sm font-medium text-gray-900">
{app.name}
</span>
</div>
</div>
<Button
variant={app.pro && !isUpgraded() ? "primary" : "secondary"}
onClick={() => handleAppClick(app)}
>
{app.pro && !isUpgraded() ? "Upgrade to Pro" : "Configure"}
</Button>
</div>
<div class="p-2">
<p class="text-xs text-gray-400">{app.description}</p>
</div>
</div>
)}
</For>
</div>
);
}
Loading

1 comment on commit 11079ae

@vercel
Copy link

@vercel vercel bot commented on 11079ae Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.