Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ APP_NAME="Network Spy (DEV)"
# APP_NAME="Network Spy (STAGING)"
# APP_NAME="Network Spy"

API_BASE_URL=

# These env is optional in development

# PostHog Analytics (Provide these in your GitHub Secrets as well)
VITE_PUBLIC_POSTHOG_KEY="your_posthog_project_api_key"
VITE_PUBLIC_POSTHOG_HOST="https://app.posthog.com"
Expand Down
9 changes: 0 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions src-tauri/src/license.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ pub fn license_check_feature(feature: String) -> bool {
let plan = cache.plan.as_deref().unwrap_or("free");
let is_personal = plan == "personal";
let is_pro = plan == "pro";
let is_licensed = is_personal || is_pro;
let is_lifetime = plan == "lifetime";
let is_licensed = is_personal || is_pro || is_lifetime;

// Check dynamic features first if they exist
if let Some(features) = &cache.features {
Expand All @@ -322,7 +323,7 @@ pub fn license_check_feature(feature: String) -> bool {

match feature.as_str() {
"scripting" | "breakpoints" | "mcp" | "premium" => is_licensed,
"custom_viewers" => is_pro,
"custom_viewers" => is_pro || is_lifetime,
_ => false,
}
}
Expand All @@ -338,7 +339,8 @@ pub fn license_get_limit(limit_name: String) -> i32 {
let plan = cache.plan.as_deref().unwrap_or("free");
let is_personal = plan == "personal";
let is_pro = plan == "pro";
let is_licensed = is_personal || is_pro;
let is_lifetime = plan == "lifetime";
let is_licensed = is_personal || is_pro || is_lifetime;

// Check dynamic features first if they exist
if let Some(features) = &cache.features {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ fn main() {
let app_name = env!("APP_NAME");

let _guard = if let Some(dsn) = option_env!("SENTRY_DSN") {
if !dsn.is_empty() {
if dsn.starts_with("http") {
Some(sentry::init((dsn, sentry::ClientOptions {
release: sentry::release_name!(),
..Default::default()
Expand Down
2 changes: 1 addition & 1 deletion src/context/AnalyticsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DevIdentifier: React.FC = () => {
export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }) => {
const apiKey = import.meta.env.VITE_PUBLIC_POSTHOG_KEY;

if (!apiKey) {
if (!apiKey || apiKey.startsWith('your_')) {
return <>{children}</>;
}

Expand Down
21 changes: 13 additions & 8 deletions src/context/FilterPresetContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { FilterNode, PredefinedFilter, FilterTypes, FilterOperators } from "../m
import { invoke } from "@tauri-apps/api/core";
import { useLicense } from "../hooks/useLicense";
import { useUpgradeDialog } from "./UpgradeContext";
import { useAtomValue } from "jotai";
import { isLicensedAtom } from "../utils/trafficAtoms";

interface FilterPresetContextState {
predefinedFilters: PredefinedFilter[];
Expand Down Expand Up @@ -105,6 +107,7 @@ export const FilterPresetProvider: React.FC<{ children: ReactNode }> = ({ childr

const { getLimit } = useLicense();
const { openUpgradeDialog } = useUpgradeDialog();
const isLicensed = useAtomValue(isLicensedAtom);

// Load from DB on mount
useEffect(() => {
Expand Down Expand Up @@ -149,14 +152,16 @@ export const FilterPresetProvider: React.FC<{ children: ReactNode }> = ({ childr
};

const addPreset = async (name: string, filters: FilterNode[], description?: string) => {
try {
const limit = await getLimit('max_filters');
if (userFilters.length >= limit) {
openUpgradeDialog();
return "";
}
} catch (e) {
console.error("Failed to check filter limit", e);
if (!isLicensed) {
try {
const limit = await getLimit('max_filters');
if (userFilters.length >= limit) {
openUpgradeDialog();
return "";
}
} catch (e) {
console.error("Failed to check filter limit", e);
}
}

const newId = `user-${Date.now()}`;
Expand Down
12 changes: 10 additions & 2 deletions src/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
import { twMerge } from "tailwind-merge";
import { useAppUpdater } from "./hooks/useAppUpdater";

import { useAtom } from 'jotai';
import { titleBarContentAtom, osAtom } from '@src/utils/trafficAtoms';
import { useAtom, useSetAtom } from 'jotai';
import { titleBarContentAtom, osAtom, isLicensedAtom } from '@src/utils/trafficAtoms';
import { useLicense } from '@src/hooks/useLicense';
import { AppPlan } from '@src/models/Plan';

export default function Layout() {
useAppUpdater();
const [customContent] = useAtom(titleBarContentAtom);
const [, setOs] = useAtom(osAtom);
const setIsLicensed = useSetAtom(isLicensedAtom);
const { getPlan } = useLicense();

useEffect(() => {
setOs(platform());
getPlan().then(plan => {
const appPlan = AppPlan.fromString(plan);
setIsLicensed(appPlan?.isPro ?? false);
});
}, []);
const [isProDialogOpen, setIsProDialogOpen] = useState(false);
const [isMainWindow] = useState(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { SettingsProvider } from "./context/SettingsProvider";
import CertificateInstaller from "./routes/certificate-installer";
import * as Sentry from "@sentry/react";

if (import.meta.env.VITE_SENTRY_DSN) {
const sentryDsn = import.meta.env.VITE_SENTRY_DSN;
if (sentryDsn && sentryDsn.startsWith('http')) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
Expand Down
5 changes: 4 additions & 1 deletion src/models/Plan.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export class AppPlan {
static readonly PERSONAL = new AppPlan("Personal");
static readonly PRO = new AppPlan("Pro");
static readonly LIFETIME = new AppPlan("Lifetime");

constructor(public readonly name: string) {}

get isPro(): boolean {
return this.name.toLowerCase() === "pro";
const n = this.name.toLowerCase();
return n === "pro" || n === "lifetime";
}

get isPersonal(): boolean {
Expand All @@ -21,6 +23,7 @@ export class AppPlan {
switch (p.toLowerCase()) {
case 'personal': return AppPlan.PERSONAL;
case 'pro': return AppPlan.PRO;
case 'lifetime': return AppPlan.LIFETIME;
default: return new AppPlan(p);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ export const ImageViewerMode = () => {

return (
<div className="bg-[#0a0a0a] flex flex-col min-h-full">
<div className="flex-grow flex items-center justify-center p-8">
{isImage ? (
{isImage ? (
<div className="flex-grow relative min-h-0">
<ImageView data={data?.body as Uint8Array} />
) : (
</div>
) : (
<div className="flex-grow flex items-center justify-center p-8">
<div className="text-zinc-500 text-sm italic">Response is not an image ({data?.content_type || 'Unknown Type'})</div>
)}
</div>
</div>
)}
</div>
);
};
Expand Down
73 changes: 71 additions & 2 deletions src/packages/bottom-pane/TabRenderer/ImageView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useEffect } from "react";
import { useMemo, useEffect, useRef, useState, useCallback } from "react";

export const ImageView = ({ data }: { data: Uint8Array }) => {
const url = useMemo(() => {
Expand All @@ -13,7 +13,76 @@ export const ImageView = ({ data }: { data: Uint8Array }) => {
};
}, [url]);

const containerRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const [scale, setScale] = useState(1);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const dragging = useRef(false);
const dragStart = useRef({ x: 0, y: 0 });
const offsetAtDragStart = useRef({ x: 0, y: 0 });

const reset = useCallback(() => {
setScale(1);
setOffset({ x: 0, y: 0 });
}, []);

const onWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setScale(prev => Math.max(0.1, Math.min(prev + delta, 10)));
}, []);

const onMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
dragging.current = true;
dragStart.current = { x: e.clientX, y: e.clientY };
offsetAtDragStart.current = offset;
}, [offset]);

const onMouseMove = useCallback((e: React.MouseEvent) => {
if (!dragging.current) return;
const dx = e.clientX - dragStart.current.x;
const dy = e.clientY - dragStart.current.y;
setOffset({
x: offsetAtDragStart.current.x + dx,
y: offsetAtDragStart.current.y + dy,
});
}, []);

const onMouseUp = useCallback(() => {
dragging.current = false;
}, []);

if (!url) return <div className="p-4 text-zinc-500 italic">No image data</div>;

return <img src={url} alt="response" className="max-w-full max-h-full object-contain" />;
return (
<div
ref={containerRef}
className="absolute inset-0 overflow-hidden bg-[#0a0a0a]"
onWheel={onWheel}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseUp}
onDoubleClick={reset}
style={{ cursor: dragging.current ? 'grabbing' : 'grab' }}
>
<img
ref={imgRef}
src={url}
alt="response"
className="max-w-full max-h-full"
style={{
transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})`,
transformOrigin: 'center center',
transition: dragging.current ? 'none' : 'transform 0.1s ease',
pointerEvents: 'none',
userSelect: 'none',
}}
/>
<div className="absolute bottom-2 right-2 bg-black/60 text-zinc-300 text-[11px] px-2 py-0.5 rounded font-mono select-none pointer-events-none">
{Math.round(scale * 100)}%
</div>
</div>
);
};
29 changes: 16 additions & 13 deletions src/packages/filter-bar/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const FilterNodeRenderer = ({

<input
type='text'
className='filter-value-input input input-xs flex-grow rounded bg-[#2a2d2c] border border-zinc-800 text-[11px] focus:outline-none focus:border-blue-500 py-0 h-6'
className='filter-value-input input input-xs flex-grow rounded bg-[#2a2d2c] border border-zinc-800 text-[11px] focus:outline-none focus:border-blue-500 py-0 h-6 px-2'
placeholder={`Search ${node.type}...`}
value={node.value}
onChange={(e) => updateNode(node.id, { value: e.target.value })}
Expand Down Expand Up @@ -171,7 +171,7 @@ const FilterNodeRenderer = ({
};

import { useAtom, useAtomValue } from "jotai";
import { mainTrafficListSearchAtom, activeTabIdAtom } from "@src/utils/trafficAtoms";
import { mainTrafficListSearchAtom, activeTabIdAtom, isLicensedAtom } from "@src/utils/trafficAtoms";

const countAllRules = (nodes: FilterNode[]): number => {
return nodes.reduce((acc, node) => {
Expand Down Expand Up @@ -200,6 +200,7 @@ export const FilterBar = () => {
const activeTabId = useAtomValue(activeTabIdAtom);
const [searchTerm, setSearchTerm] = useAtom(mainTrafficListSearchAtom(activeTabId));
const [errorMsg, setErrorMsg] = React.useState<string | null>(null);
const isLicensed = useAtomValue(isLicensedAtom);

React.useEffect(() => {
if (errorMsg) {
Expand All @@ -208,8 +209,6 @@ export const FilterBar = () => {
}
}, [errorMsg]);



const { appPredefined, userPredefined } = React.useMemo(() => {
const filtered = predefinedFilters.filter(f =>
f.name.toLowerCase().includes(searchTerm.toLowerCase())
Expand All @@ -221,10 +220,12 @@ export const FilterBar = () => {
}, [predefinedFilters, searchTerm]);

const addRule = async (parentId: string | null = null) => {
const limit = await getLimit('max_filters');
if (countAllRules(filters) >= limit) {
openUpgradeDialog();
return;
if (!isLicensed) {
const limit = await getLimit('max_filters');
if (countAllRules(filters) >= limit) {
openUpgradeDialog();
return;
}
}

const newRule: FilterRule = {
Expand Down Expand Up @@ -256,10 +257,12 @@ export const FilterBar = () => {
};

const addGroup = async (parentId: string | null = null) => {
const limit = await getLimit('max_filters');
if (countAllRules(filters) >= limit) {
openUpgradeDialog();
return;
if (!isLicensed) {
const limit = await getLimit('max_filters');
if (countAllRules(filters) >= limit) {
openUpgradeDialog();
return;
}
}

const newGroup: FilterGroup = {
Expand Down Expand Up @@ -398,7 +401,7 @@ export const FilterBar = () => {
<input
type="text"
placeholder="Search filters..."
className="bg-transparent border-none outline-none text-[11px] text-zinc-300 ml-2 w-20 focus:w-32 transition-all duration-300"
className="bg-transparent border-none outline-none text-[11px] text-zinc-300 ml-2 px-1 w-20 focus:w-32 transition-all duration-300"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/utils/trafficAtoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ export const statusInfoDialogAtom = atom<{
export const titleBarContentAtom = atom<React.ReactNode | null>(null);

export const osAtom = atom<string>('macos');

export const isLicensedAtom = atom<boolean | null>(null);
Loading