Compare commits
No commits in common. "development_ipfs" and "main" have entirely different histories.
developmen
...
main
69
impl.md
69
impl.md
@ -1,69 +0,0 @@
|
||||
# Implementation Status Report
|
||||
|
||||
## Analysis of Implementation Status
|
||||
|
||||
### What Has Been Implemented:
|
||||
|
||||
1. **Basic Website Structure**:
|
||||
- Svelte-based frontend with components for layout, navigation, and content display
|
||||
- Responsive design with mobile support
|
||||
- Markdown content rendering capability
|
||||
|
||||
2. **IPFS Integration**:
|
||||
- IPFS service implementation using Helia library
|
||||
- Content retrieval from IPFS
|
||||
- Content upload to IPFS
|
||||
- Fallback to IPFS gateways when direct connection fails
|
||||
- Local caching of IPFS content using IndexedDB
|
||||
- Network status monitoring for offline support
|
||||
|
||||
3. **UI Components**:
|
||||
- Header/Navbar
|
||||
- Sidebar navigation
|
||||
- Footer
|
||||
- Markdown content display
|
||||
- IPFS demo functionality
|
||||
|
||||
4. **Demo Functionality**:
|
||||
- IPFS content upload and retrieval demo
|
||||
- Mock functionality when IPFS is not available
|
||||
|
||||
### What Is Missing:
|
||||
|
||||
1. **Content Processing Pipeline**:
|
||||
- No implementation of the Blake hashing algorithm for content
|
||||
- No implementation of content encryption/decryption
|
||||
- No CLI tools or scripts for processing content files as described in specs
|
||||
- Missing the pipeline for file discovery, normalization, hashing, encryption, and IPFS upload
|
||||
|
||||
2. **Metadata Structure**:
|
||||
- No implementation of the metadata structure as described in specs
|
||||
- Missing the pages list metadata with Blake hash + IPFS CID format
|
||||
- No implementation of metadata retrieval from IPFS
|
||||
|
||||
3. **Security Features**:
|
||||
- Missing the encryption/decryption functionality using Blake hash as key
|
||||
- No implementation of the security considerations mentioned in specs
|
||||
|
||||
4. **Collection Structure**:
|
||||
- No implementation of the collection structure as described in specs
|
||||
- Missing the `.collection` file handling
|
||||
|
||||
5. **Content Retrieval with Decryption**:
|
||||
- No implementation of retrieving encrypted content and decrypting it
|
||||
|
||||
## Conclusion
|
||||
|
||||
The project has implemented a solid foundation for a browser-based website with IPFS integration, including:
|
||||
- Basic website structure and UI components
|
||||
- IPFS content retrieval and upload
|
||||
- Local caching and offline support
|
||||
- Demo functionality
|
||||
|
||||
However, the core security features described in the specifications are not implemented:
|
||||
- The content processing pipeline with Blake hashing and encryption
|
||||
- The metadata structure with combined Blake hash and IPFS CID
|
||||
- The collection structure for organizing content
|
||||
- The encryption/decryption functionality
|
||||
|
||||
The current implementation provides a working demo of IPFS integration but lacks the security features that are central to the project's specifications. To fully meet the requirements, the missing components would need to be implemented, particularly the content processing pipeline with encryption and the metadata structure.
|
File diff suppressed because it is too large
Load Diff
1361
sweb/bun.lock
1361
sweb/bun.lock
File diff suppressed because it is too large
Load Diff
@ -26,22 +26,12 @@
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@helia/interface": "^5.2.1",
|
||||
"@helia/unixfs": "^5.0.0",
|
||||
"@libp2p/webrtc": "^5.2.12",
|
||||
"@libp2p/websockets": "^9.2.10",
|
||||
"@libp2p/webtransport": "^5.0.40",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@tailwindcss/postcss": "^4.1.6",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"helia": "^5.3.0",
|
||||
"idb": "^8.0.3",
|
||||
"libp2p": "^2.8.5",
|
||||
"lucide-svelte": "^0.509.0",
|
||||
"marked": "^15.0.11",
|
||||
"multiformats": "^13.3.3",
|
||||
"shadcn-svelte": "^0.14.2",
|
||||
"tailwind-merge": "^3.2.0"
|
||||
}
|
||||
|
4919
sweb/pnpm-lock.yaml
4919
sweb/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Layout from "./components/Layout.svelte";
|
||||
import Home from "./components/Home.svelte";
|
||||
import OfflineStatus from "./components/OfflineStatus.svelte";
|
||||
import * as IPFSProviderModule from "./components/IPFSProvider.svelte";
|
||||
import "./app.css";
|
||||
|
||||
// Use the component from the module
|
||||
const IPFSProvider = IPFSProviderModule.default;
|
||||
</script>
|
||||
|
||||
<IPFSProvider>
|
||||
<Layout>
|
||||
<Home contentPath="" />
|
||||
</Layout>
|
||||
<OfflineStatus />
|
||||
</IPFSProvider>
|
||||
<Layout>
|
||||
<Home />
|
||||
</Layout>
|
||||
|
@ -1,198 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cryptoService } from '../services/crypto.service';
|
||||
import { contentProcessorService } from '../services/content-processor.service';
|
||||
import { ipfsService } from '../services/ipfs.service';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let textInput = '';
|
||||
let blakeHash = '';
|
||||
let selectedFile: File | null = null;
|
||||
let fileBlakeHash = '';
|
||||
let normalizedFilename = '';
|
||||
let ipfsCid = '';
|
||||
let combinedKey = '';
|
||||
let isProcessing = false;
|
||||
let errorMessage = '';
|
||||
let successMessage = '';
|
||||
|
||||
// Hash the text input using Blake3
|
||||
function hashText() {
|
||||
try {
|
||||
errorMessage = '';
|
||||
if (!textInput.trim()) {
|
||||
errorMessage = 'Please enter some text to hash';
|
||||
return;
|
||||
}
|
||||
blakeHash = cryptoService.hashStringBlake3AsHex(textInput);
|
||||
successMessage = 'Text hashed successfully!';
|
||||
} catch (error) {
|
||||
console.error('Error hashing text:', error);
|
||||
errorMessage = `Error hashing text: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file selection
|
||||
function handleFileSelect(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
selectedFile = input.files[0];
|
||||
// Reset previous results
|
||||
fileBlakeHash = '';
|
||||
normalizedFilename = '';
|
||||
ipfsCid = '';
|
||||
combinedKey = '';
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Process the selected file
|
||||
async function processFile() {
|
||||
if (!selectedFile) {
|
||||
errorMessage = 'Please select a file to process';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isProcessing = true;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
|
||||
// Normalize the filename
|
||||
normalizedFilename = contentProcessorService.normalizeFilename(selectedFile.name);
|
||||
|
||||
// Hash the file using Blake3
|
||||
fileBlakeHash = await cryptoService.hashFileBlake3AsHex(selectedFile);
|
||||
|
||||
// Process the file through the content processing pipeline
|
||||
const metadata = await contentProcessorService.processFile(selectedFile);
|
||||
|
||||
// Update the UI with the results
|
||||
ipfsCid = metadata.ipfsCid;
|
||||
combinedKey = metadata.combinedKey;
|
||||
|
||||
isProcessing = false;
|
||||
successMessage = 'File processed successfully!';
|
||||
} catch (error) {
|
||||
console.error('Error processing file:', error);
|
||||
errorMessage = `Error processing file: ${error instanceof Error ? error.message : String(error)}`;
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize IPFS when the component mounts
|
||||
onMount(async () => {
|
||||
try {
|
||||
await ipfsService.initialize();
|
||||
} catch (error) {
|
||||
console.error('Error initializing IPFS:', error);
|
||||
errorMessage = `Error initializing IPFS: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="p-6 max-w-4xl mx-auto bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6">Blake Hashing Demo</h2>
|
||||
|
||||
<!-- Text Input Section -->
|
||||
<div class="mb-8 p-4 border border-gray-200 rounded-md">
|
||||
<h3 class="text-xl font-semibold mb-4">Text Hashing</h3>
|
||||
<div class="mb-4">
|
||||
<label for="textInput" class="block text-sm font-medium text-gray-700 mb-1">Enter text to hash:</label>
|
||||
<textarea
|
||||
id="textInput"
|
||||
bind:value={textInput}
|
||||
class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
on:click={hashText}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Hash Text
|
||||
</button>
|
||||
{#if blakeHash}
|
||||
<div class="mt-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">Blake3 Hash:</h4>
|
||||
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
|
||||
<code class="text-sm break-all">{blakeHash}</code>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- File Processing Section -->
|
||||
<div class="p-4 border border-gray-200 rounded-md">
|
||||
<h3 class="text-xl font-semibold mb-4">File Processing</h3>
|
||||
<div class="mb-4">
|
||||
<label for="fileInput" class="block text-sm font-medium text-gray-700 mb-1">Select a file to process:</label>
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
on:change={handleFileSelect}
|
||||
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
on:click={processFile}
|
||||
disabled={!selectedFile || isProcessing}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-blue-300 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessing ? 'Processing...' : 'Process File'}
|
||||
</button>
|
||||
|
||||
{#if selectedFile && (normalizedFilename || fileBlakeHash || ipfsCid || combinedKey)}
|
||||
<div class="mt-4 space-y-3">
|
||||
{#if normalizedFilename}
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">Normalized Filename:</h4>
|
||||
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
|
||||
<code class="text-sm break-all">{normalizedFilename}</code>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if fileBlakeHash}
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">Blake3 Hash:</h4>
|
||||
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
|
||||
<code class="text-sm break-all">{fileBlakeHash}</code>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if ipfsCid}
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">IPFS CID:</h4>
|
||||
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
|
||||
<code class="text-sm break-all">{ipfsCid}</code>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if combinedKey}
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">Combined Key (Blake Hash + IPFS CID):</h4>
|
||||
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
|
||||
<code class="text-sm break-all">{combinedKey}</code>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{#if errorMessage}
|
||||
<div class="mt-4 p-3 bg-red-100 text-red-700 rounded-md">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if successMessage}
|
||||
<div class="mt-4 p-3 bg-green-100 text-green-700 rounded-md">
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import MarkdownContent from "./MarkdownContent.svelte";
|
||||
import { Shield, Zap, Smartphone } from "lucide-svelte";
|
||||
|
||||
export let contentPath: string = "introduction/introduction";
|
||||
|
||||
@ -8,24 +9,169 @@
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
{#if contentPath && contentPath.trim() !== ""}
|
||||
{#if contentPath}
|
||||
<div
|
||||
class="bg-background-secondary shadow-sm p-4 sm:p-6 overflow-x-auto"
|
||||
>
|
||||
<MarkdownContent path={actualPath} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-8 text-center">
|
||||
<h2 class="text-2xl sm:text-3xl font-bold mb-4 text-text">
|
||||
Welcome to SecureWeb
|
||||
</h2>
|
||||
<p class="text-lg text-text-secondary max-w-3xl mx-auto">
|
||||
A decentralized web platform with IPFS integration and Blake
|
||||
hashing for content security.
|
||||
<!-- Hero Section -->
|
||||
<section class="mb-12 sm:mb-16 md:mb-20 text-center md:text-left">
|
||||
<div class="md:flex md:items-center md:justify-between">
|
||||
<div class="md:w-1/2 mb-8 md:mb-0 md:pr-6">
|
||||
<h1
|
||||
class="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 sm:mb-6 text-text leading-tight"
|
||||
>
|
||||
Welcome to <span class="text-primary-600"
|
||||
>SecureWeb</span
|
||||
>
|
||||
</h1>
|
||||
<p
|
||||
class="text-lg sm:text-xl text-text-secondary max-w-2xl mb-6 sm:mb-8 leading-relaxed"
|
||||
>
|
||||
A comprehensive platform for secure and reliable web
|
||||
solutions designed for modern businesses.
|
||||
</p>
|
||||
<p class="mt-4 text-text-secondary">
|
||||
Use the sidebar navigation to explore the content and demos.
|
||||
<div
|
||||
class="flex flex-col xs:flex-row justify-center md:justify-start gap-3 sm:gap-4"
|
||||
>
|
||||
<button
|
||||
class="bg-primary-600 hover:bg-primary-700 text-white px-6 sm:px-8 py-2.5 sm:py-3 rounded-md font-medium transition-colors duration-200 w-full xs:w-auto"
|
||||
>Get Started</button
|
||||
>
|
||||
<button
|
||||
class="bg-background-secondary hover:bg-background text-text px-6 sm:px-8 py-2.5 sm:py-3 rounded-md font-medium border border-border transition-colors duration-200 w-full xs:w-auto"
|
||||
>Learn More</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:w-1/2">
|
||||
<img
|
||||
src="https://via.placeholder.com/600x400/f3f4f6/1e40af?text=SecureWeb"
|
||||
alt="SecureWeb Platform"
|
||||
class="rounded-lg shadow-lg w-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="mb-12 sm:mb-16 md:mb-20">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2
|
||||
class="text-2xl sm:text-3xl font-bold mb-3 sm:mb-4 text-text"
|
||||
>
|
||||
Our Features
|
||||
</h2>
|
||||
<p
|
||||
class="text-lg sm:text-xl text-text-secondary max-w-3xl mx-auto"
|
||||
>
|
||||
Discover what makes SecureWeb the preferred choice for
|
||||
security-conscious businesses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 sm:gap-8 md:gap-10"
|
||||
>
|
||||
<div
|
||||
class="bg-background p-6 sm:p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow border border-border"
|
||||
>
|
||||
<div
|
||||
class="bg-primary-100 p-3 rounded-full w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center mb-4 sm:mb-6"
|
||||
>
|
||||
<Shield
|
||||
class="h-6 w-6 sm:h-7 sm:w-7 text-primary-600"
|
||||
/>
|
||||
</div>
|
||||
<h3
|
||||
class="text-lg sm:text-xl font-bold mb-3 sm:mb-4 text-text"
|
||||
>
|
||||
Advanced Security
|
||||
</h3>
|
||||
<p
|
||||
class="text-text-secondary leading-relaxed text-sm sm:text-base"
|
||||
>
|
||||
State-of-the-art encryption and security protocols to
|
||||
keep your data protected from threats.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-background-secondary p-6 sm:p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow border border-border"
|
||||
>
|
||||
<div
|
||||
class="bg-primary-100 p-3 rounded-full w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center mb-4 sm:mb-6"
|
||||
>
|
||||
<Zap class="h-6 w-6 sm:h-7 sm:w-7 text-primary-600" />
|
||||
</div>
|
||||
<h3
|
||||
class="text-lg sm:text-xl font-bold mb-3 sm:mb-4 text-text"
|
||||
>
|
||||
Lightning Performance
|
||||
</h3>
|
||||
<p
|
||||
class="text-text-secondary leading-relaxed text-sm sm:text-base"
|
||||
>
|
||||
Optimized for speed and efficiency, ensuring a smooth
|
||||
and responsive user experience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-background p-6 sm:p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow border border-border sm:col-span-2 md:col-span-1"
|
||||
>
|
||||
<div
|
||||
class="bg-primary-100 p-3 rounded-full w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center mb-4 sm:mb-6"
|
||||
>
|
||||
<Smartphone
|
||||
class="h-6 w-6 sm:h-7 sm:w-7 text-primary-600"
|
||||
/>
|
||||
</div>
|
||||
<h3
|
||||
class="text-lg sm:text-xl font-bold mb-3 sm:mb-4 text-text"
|
||||
>
|
||||
Responsive Design
|
||||
</h3>
|
||||
<p
|
||||
class="text-text-secondary leading-relaxed text-sm sm:text-base"
|
||||
>
|
||||
Fully responsive layouts that provide a seamless
|
||||
experience across all devices and screen sizes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section>
|
||||
<div
|
||||
class="bg-gradient-to-r from-primary-600 to-primary-800 rounded-xl sm:rounded-2xl p-6 sm:p-8 md:p-10 text-white shadow-lg sm:shadow-xl"
|
||||
>
|
||||
<div class="md:flex md:items-center md:justify-between">
|
||||
<div class="mb-6 md:mb-0 md:w-2/3 md:pr-6">
|
||||
<h2 class="text-2xl sm:text-3xl font-bold mb-3 sm:mb-4">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<p
|
||||
class="text-base sm:text-lg text-primary-100 mb-0 max-w-2xl"
|
||||
>
|
||||
Join thousands of satisfied users who trust
|
||||
SecureWeb for their web security needs. Sign up
|
||||
today and experience the difference.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center md:justify-end">
|
||||
<button
|
||||
class="bg-background-secondary text-primary-700 hover:bg-primary-50 px-6 sm:px-8 py-2.5 sm:py-3 rounded-lg font-semibold text-base sm:text-lg shadow-md transition-colors w-full xs:w-auto"
|
||||
>
|
||||
Sign Up Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,152 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ipfsService } from "../services/ipfs.service";
|
||||
import { onMount } from "svelte";
|
||||
import MarkdownContent from "./MarkdownContent.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
let content = "";
|
||||
let uploadedCid = "";
|
||||
let retrievedCid = "";
|
||||
let uploading = false;
|
||||
let uploadError: string | null = null;
|
||||
let showPreview = false;
|
||||
|
||||
async function uploadContent() {
|
||||
console.log("Upload button clicked");
|
||||
if (!content.trim()) {
|
||||
uploadError = "Content cannot be empty";
|
||||
console.log("Content is empty, not uploading");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Starting upload process");
|
||||
uploading = true;
|
||||
uploadError = null;
|
||||
|
||||
try {
|
||||
console.log(
|
||||
"Calling ipfsService.uploadContent with content:",
|
||||
content.substring(0, 50) + "...",
|
||||
);
|
||||
uploadedCid = await ipfsService.uploadContent(content);
|
||||
console.log("Content uploaded successfully with CID:", uploadedCid);
|
||||
} catch (err: any) {
|
||||
uploadError = err.message || "Failed to upload content";
|
||||
console.error("Upload error:", err);
|
||||
} finally {
|
||||
console.log("Upload process completed, uploading =", uploading);
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetDemo() {
|
||||
content = "";
|
||||
uploadedCid = "";
|
||||
retrievedCid = "";
|
||||
uploadError = null;
|
||||
showPreview = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
console.log("IPFSDemo component mounted");
|
||||
// Check if IPFS is initialized
|
||||
const isInitialized = ipfsService.isInitialized();
|
||||
console.log("IPFS initialized status:", isInitialized);
|
||||
|
||||
if (!isInitialized) {
|
||||
uploadError =
|
||||
"IPFS is not initialized. Please wait or refresh the page.";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="max-w-3xl mx-auto p-4">
|
||||
<h2 class="text-2xl font-semibold mb-6 text-primary-700">IPFS Demo</h2>
|
||||
|
||||
<div class="bg-background-secondary rounded-lg p-6 mb-8 shadow-sm">
|
||||
<h3 class="text-xl font-medium mb-4 text-primary-600">
|
||||
Upload Content to IPFS
|
||||
</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="content" class="block mb-2 font-medium"
|
||||
>Enter Markdown Content:</label
|
||||
>
|
||||
<textarea
|
||||
id="content"
|
||||
bind:value={content}
|
||||
class="w-full p-3 border border-border rounded-md font-inherit resize-y bg-background-secondary text-text"
|
||||
rows="8"
|
||||
placeholder="# Hello IPFS This is a test of IPFS content storage and retrieval."
|
||||
disabled={uploading}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mb-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
on:click={() => {
|
||||
console.log("Upload button clicked directly");
|
||||
uploadContent();
|
||||
}}
|
||||
disabled={uploading || !content.trim()}
|
||||
>
|
||||
{uploading ? "Uploading..." : "Upload to IPFS"}
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" on:click={resetDemo}>Reset</Button>
|
||||
</div>
|
||||
|
||||
{#if uploadError}
|
||||
<div
|
||||
class="mt-4 p-3 bg-red-50 border-l-4 border-red-600 text-red-700 rounded"
|
||||
>
|
||||
{uploadError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if uploadedCid}
|
||||
<div
|
||||
class="mt-4 p-3 bg-green-50 border-l-4 border-green-600 text-green-800 rounded"
|
||||
>
|
||||
<p>Content uploaded successfully!</p>
|
||||
<p>
|
||||
CID: <code
|
||||
class="bg-black/5 px-2 py-0.5 rounded font-mono break-all"
|
||||
>{uploadedCid}</code
|
||||
>
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
on:click={() => {
|
||||
retrievedCid = uploadedCid;
|
||||
showPreview = true;
|
||||
}}
|
||||
>
|
||||
Preview Content
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showPreview && retrievedCid}
|
||||
<div class="bg-background-secondary rounded-lg p-6 mb-8 shadow-sm">
|
||||
<h3 class="text-xl font-medium mb-4 text-primary-600">
|
||||
Content Preview
|
||||
</h3>
|
||||
<p>
|
||||
Retrieving content with CID: <code
|
||||
class="bg-black/5 px-2 py-0.5 rounded font-mono"
|
||||
>{retrievedCid}</code
|
||||
>
|
||||
</p>
|
||||
|
||||
<div class="bg-white rounded-md p-4 mt-4 border border-border">
|
||||
<MarkdownContent contentCid={retrievedCid} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
@ -1,107 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { ipfsService } from "../services/ipfs.service";
|
||||
|
||||
let initialized = false;
|
||||
let initializing = true;
|
||||
let error: string | null = null;
|
||||
let mockMode = false;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
initializing = true;
|
||||
initialized = await ipfsService.initialize();
|
||||
console.log("IPFS initialization status:", initialized);
|
||||
|
||||
// Even if initialization returns true, check if IPFS is actually initialized
|
||||
if (!ipfsService.isInitialized()) {
|
||||
console.warn(
|
||||
"IPFS service reports success but is not fully initialized",
|
||||
);
|
||||
mockMode = true;
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || "Unknown error during IPFS initialization";
|
||||
console.error("Failed to initialize IPFS:", err);
|
||||
mockMode = true;
|
||||
} finally {
|
||||
initializing = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if initializing}
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 bg-black/70 text-white py-2 px-4 z-50 flex justify-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="animate-spin"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
||||
</svg>
|
||||
<span>Initializing IPFS...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 bg-red-600/90 text-white py-2 px-4 z-50 flex justify-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span
|
||||
>IPFS initialization issue: {error} - Running in demo mode</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if mockMode}
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 bg-amber-500/90 text-white py-2 px-4 z-50 flex justify-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
<span>IPFS running in demo mode with mock functionality</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot />
|
@ -4,8 +4,6 @@
|
||||
import Footer from "./Footer.svelte";
|
||||
import NavDataProvider from "./NavDataProvider.svelte";
|
||||
import Home from "./Home.svelte";
|
||||
import BlakeHashDemo from "./BlakeHashDemo.svelte";
|
||||
import IPFSDemo from "./IPFSDemo.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import ThemeProvider from "../lib/theme/ThemeProvider.svelte";
|
||||
|
||||
@ -84,15 +82,7 @@
|
||||
sidebarVisible && !isMobile ? "md:ml-64" : "ml-0"
|
||||
}`}
|
||||
>
|
||||
{#if selectedContentPath === "demos/blake-hash-demo"}
|
||||
<div class="p-4">
|
||||
<BlakeHashDemo />
|
||||
</div>
|
||||
{:else if selectedContentPath === "demos/ipfs-demo"}
|
||||
<div class="p-4">
|
||||
<IPFSDemo />
|
||||
</div>
|
||||
{:else if selectedContentPath}
|
||||
{#if selectedContentPath}
|
||||
<Home contentPath={selectedContentPath} />
|
||||
{:else}
|
||||
<slot />
|
||||
|
@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { marked } from "marked";
|
||||
import { ipfsService } from "../services/ipfs.service";
|
||||
|
||||
export let path: string = "";
|
||||
export let contentCid: string = "";
|
||||
|
||||
// Base path for images
|
||||
const imagePath = "/images";
|
||||
@ -13,39 +11,22 @@
|
||||
let loading = true;
|
||||
let error: string | null = null;
|
||||
|
||||
$: if (path || contentCid) {
|
||||
loadContent();
|
||||
$: if (path) {
|
||||
loadMarkdownContent(path);
|
||||
}
|
||||
|
||||
async function loadContent() {
|
||||
async function loadMarkdownContent(mdPath: string) {
|
||||
if (!mdPath) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
content = "";
|
||||
|
||||
try {
|
||||
let markdown = "";
|
||||
|
||||
if (contentCid) {
|
||||
// Load from IPFS
|
||||
console.log(
|
||||
`Loading content from IPFS with CID: ${contentCid}`,
|
||||
);
|
||||
try {
|
||||
markdown = await ipfsService.getContent(contentCid);
|
||||
} catch (err: any) {
|
||||
throw new Error(
|
||||
`Failed to load content from IPFS: ${err.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Process IPFS image references if any
|
||||
markdown = await processIPFSImageReferences(markdown);
|
||||
} else if (path) {
|
||||
// Load from local file system
|
||||
// Remove leading slash if present
|
||||
const cleanPath = path.startsWith("/")
|
||||
? path.substring(1)
|
||||
: path;
|
||||
const cleanPath = mdPath.startsWith("/")
|
||||
? mdPath.substring(1)
|
||||
: mdPath;
|
||||
|
||||
// If path is just a section like "introduction", append "/introduction" to it
|
||||
const finalPath = cleanPath.includes("/")
|
||||
@ -62,12 +43,9 @@
|
||||
}
|
||||
|
||||
// Get the directory path for relative image references
|
||||
const docDir = finalPath.substring(
|
||||
0,
|
||||
finalPath.lastIndexOf("/"),
|
||||
);
|
||||
const docDir = finalPath.substring(0, finalPath.lastIndexOf("/"));
|
||||
|
||||
markdown = await response.text();
|
||||
let markdown = await response.text();
|
||||
|
||||
// Process markdown to fix image paths
|
||||
// Replace relative image paths with absolute paths
|
||||
@ -77,9 +55,6 @@
|
||||
return ``;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw new Error("No path or CID provided");
|
||||
}
|
||||
|
||||
const parsedContent = marked.parse(markdown);
|
||||
content =
|
||||
@ -87,154 +62,208 @@
|
||||
? parsedContent
|
||||
: await parsedContent;
|
||||
} catch (err: any) {
|
||||
console.error("Error loading content:", err);
|
||||
console.error("Error loading markdown content:", err);
|
||||
error = err.message || "Failed to load content";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process IPFS image references in markdown content
|
||||
* @param markdown - Markdown content with IPFS image references
|
||||
* @returns Processed markdown with IPFS images replaced with blob URLs
|
||||
*/
|
||||
async function processIPFSImageReferences(
|
||||
markdown: string,
|
||||
): Promise<string> {
|
||||
// This is a simplified example - actual implementation would depend on how images are referenced
|
||||
// For example, if images are referenced as ipfs://Qm...
|
||||
const ipfsImageRegex = /!\[(.*?)\]\(ipfs:\/\/(.*?)\)/g;
|
||||
|
||||
const promises: Promise<string>[] = [];
|
||||
let match;
|
||||
const replacements: [string, string][] = [];
|
||||
|
||||
while ((match = ipfsImageRegex.exec(markdown)) !== null) {
|
||||
const [fullMatch, alt, cid] = match;
|
||||
|
||||
promises.push(
|
||||
(async () => {
|
||||
try {
|
||||
const imageBlob = await ipfsService.getImage(cid);
|
||||
const imageUrl = URL.createObjectURL(imageBlob);
|
||||
replacements.push([
|
||||
fullMatch,
|
||||
``,
|
||||
]);
|
||||
return "success";
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load image with CID ${cid}:`,
|
||||
error,
|
||||
);
|
||||
return "error";
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
let processedMarkdown = markdown;
|
||||
for (const [original, replacement] of replacements) {
|
||||
processedMarkdown = processedMarkdown.replace(
|
||||
original,
|
||||
replacement,
|
||||
);
|
||||
}
|
||||
|
||||
return processedMarkdown;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (path || contentCid) {
|
||||
loadContent();
|
||||
if (path) {
|
||||
loadMarkdownContent(path);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="max-w-full">
|
||||
<div class="markdown-content">
|
||||
{#if loading}
|
||||
<div class="h-screen flex justify-center items-center p-4 sm:p-8">
|
||||
<div class="loading">
|
||||
<p>Loading content...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-center p-4 sm:p-8">
|
||||
<div class="error">
|
||||
<p>Error: {error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="markdown-content text-green-800">
|
||||
<div class="content">
|
||||
{@html content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Using @apply for markdown content styling since we need to target elements within the rendered HTML */
|
||||
.markdown-content :global(h1) {
|
||||
@apply text-xl font-bold mb-4 text-primary-700 break-words sm:text-3xl sm:mb-6;
|
||||
.markdown-content {
|
||||
/* padding: 0.5rem; */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown-content :global(h2) {
|
||||
@apply text-lg font-semibold mt-6 mb-3 text-primary-800 break-words sm:text-2xl sm:mt-8 sm:mb-4;
|
||||
@media (min-width: 640px) {
|
||||
.markdown-content {
|
||||
/* padding: 1rem; */
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-content :global(h3) {
|
||||
@apply text-base font-semibold mt-5 mb-2 text-primary-800 break-words sm:text-xl sm:mt-6 sm:mb-3;
|
||||
.loading,
|
||||
.error {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-content :global(p) {
|
||||
@apply mb-4 leading-relaxed;
|
||||
@media (min-width: 640px) {
|
||||
.loading,
|
||||
.error {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-content :global(ul),
|
||||
.markdown-content :global(ol) {
|
||||
@apply mb-4 ml-6 sm:ml-8;
|
||||
.error {
|
||||
color: rgb(229 62 62);
|
||||
}
|
||||
|
||||
.markdown-content :global(li) {
|
||||
@apply mb-2;
|
||||
.content :global(h1) {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
color: rgb(var(--color-primary-700));
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.markdown-content :global(a) {
|
||||
@apply text-primary-600 no-underline break-words hover:underline;
|
||||
.content :global(h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: rgb(var(--color-primary-800));
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.markdown-content :global(blockquote) {
|
||||
@apply border-l-4 border-border pl-4 italic text-text-secondary;
|
||||
.content :global(h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: rgb(var(--color-primary-800));
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.markdown-content :global(code) {
|
||||
@apply bg-background-secondary py-0.5 px-1.5 rounded text-sm font-mono break-words whitespace-pre-wrap;
|
||||
.content :global(p) {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content :global(pre) {
|
||||
@apply bg-background-secondary p-3 sm:p-4 rounded-lg overflow-x-auto mb-4;
|
||||
.content :global(ul),
|
||||
.content :global(ol) {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown-content :global(pre code) {
|
||||
@apply bg-transparent p-0 whitespace-pre break-normal;
|
||||
.content :global(li) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-content :global(img) {
|
||||
@apply max-w-full h-auto rounded-lg my-4 block;
|
||||
.content :global(a) {
|
||||
color: rgb(var(--color-primary-600));
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.markdown-content :global(hr) {
|
||||
@apply border-0 border-t border-border my-6;
|
||||
.content :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-content :global(table) {
|
||||
@apply w-full border-collapse mb-4 block overflow-x-auto;
|
||||
.content :global(blockquote) {
|
||||
border-left: 4px solid rgb(var(--color-border));
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-style: italic;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.markdown-content :global(th),
|
||||
.markdown-content :global(td) {
|
||||
@apply border border-border p-2 min-w-[100px];
|
||||
.content :global(code) {
|
||||
background-color: rgb(var(--color-background-secondary));
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.875em;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.markdown-content :global(th) {
|
||||
@apply bg-background-secondary;
|
||||
.content :global(pre) {
|
||||
background-color: rgb(var(--color-background-secondary));
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content :global(pre code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
white-space: pre;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.content :global(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content :global(hr) {
|
||||
border: 0;
|
||||
border-top: 1px solid rgb(var(--color-border));
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.content :global(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.content :global(th),
|
||||
.content :global(td) {
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
padding: 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.content :global(th) {
|
||||
background-color: rgb(var(--color-background-secondary));
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.content :global(h1) {
|
||||
font-size: 2.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.content :global(h2) {
|
||||
font-size: 1.75rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content :global(h3) {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.content :global(ul),
|
||||
.content :global(ol) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.content :global(pre) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -2,7 +2,6 @@
|
||||
import type { NavItem } from "../types/nav";
|
||||
import { ChevronRight, ChevronDown } from "lucide-svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
export let item: NavItem;
|
||||
export let level: number = 0;
|
||||
@ -30,40 +29,27 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<div class="nav-item" style="--indent: {indentation}px;">
|
||||
{#if hasChildren}
|
||||
<!-- Folder item -->
|
||||
<button
|
||||
class="flex items-center w-full text-left py-2 px-3 pl-[calc({indentation}px+12px)] relative border-none bg-transparent text-text-secondary text-sm cursor-pointer transition-colors duration-200 hover:bg-background/50 {isActive
|
||||
? 'bg-primary-500/10 text-primary-600 font-medium'
|
||||
: ''}"
|
||||
class="nav-button folder-button {isActive ? 'active' : ''}"
|
||||
on:click={() => onToggle(item.link)}
|
||||
on:keydown={(e) => handleKeyDown(e, true)}
|
||||
tabindex="0"
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div
|
||||
class="absolute left-0 top-0 bottom-0 pointer-events-none"
|
||||
style="width: {indentation}px;"
|
||||
>
|
||||
<div
|
||||
class="absolute left-3 top-0 bottom-0 w-px bg-border"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex items-center mr-2">
|
||||
<div class="tree-line" style="width: {indentation}px;"></div>
|
||||
<div class="icon-container">
|
||||
<ChevronRight
|
||||
class="w-4 h-4 transition-transform duration-200 {isExpanded
|
||||
? 'rotate-90'
|
||||
: ''}"
|
||||
class="chevron-icon {isExpanded ? 'expanded' : ''}"
|
||||
/>
|
||||
</div>
|
||||
<span class="whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
>{item.label}</span
|
||||
>
|
||||
<span class="label">{item.label}</span>
|
||||
</button>
|
||||
|
||||
{#if isExpanded && item.children && item.children.length > 0}
|
||||
<div class="overflow-hidden" transition:slide={{ duration: 200 }}>
|
||||
<div class="children" transition:slide={{ duration: 200 }}>
|
||||
{#each item.children as child}
|
||||
<svelte:self
|
||||
item={child}
|
||||
@ -80,27 +66,94 @@
|
||||
<!-- File item -->
|
||||
<a
|
||||
href={item.link}
|
||||
class="flex items-center w-full text-left py-2 px-3 pl-[calc({indentation}px+12px)] relative border-none bg-transparent text-text-secondary text-sm cursor-pointer transition-colors duration-200 hover:bg-background/50 {isActive
|
||||
? 'bg-primary-500/10 text-primary-600 font-medium'
|
||||
: ''}"
|
||||
class="nav-button file-button {isActive ? 'active' : ''}"
|
||||
on:click={(e) => onNavClick(item.link, e)}
|
||||
on:keydown={(e) => handleKeyDown(e, false)}
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="absolute left-0 top-0 bottom-0 pointer-events-none"
|
||||
style="width: {indentation}px;"
|
||||
>
|
||||
<div
|
||||
class="absolute left-3 top-0 bottom-0 w-px bg-border"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex items-center mr-2">
|
||||
<div class="tree-line" style="width: {indentation}px;"></div>
|
||||
<div class="icon-container">
|
||||
<!-- No file icon -->
|
||||
</div>
|
||||
<span class="whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
>{item.label}</span
|
||||
>
|
||||
<span class="label">{item.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.nav-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
padding-left: calc(var(--indent) + 12px);
|
||||
position: relative;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background-color: rgba(var(--color-background), 0.5);
|
||||
}
|
||||
|
||||
.nav-button.active {
|
||||
background-color: rgba(var(--color-primary-500), 0.1);
|
||||
color: rgb(var(--color-primary-600));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tree-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-line::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: rgb(var(--color-border));
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
:global(.chevron-icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
:global(.chevron-icon.expanded) {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.children {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Menu, Search, X } from "lucide-svelte";
|
||||
import ThemeToggle from "../lib/theme/ThemeToggle.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
export let toggleSidebar: () => void = () => {};
|
||||
export let isMobile: boolean = false;
|
||||
@ -12,10 +11,8 @@
|
||||
class="bg-background border-b border-border fixed top-0 left-0 right-0 z-30 h-16 flex items-center justify-between px-3 sm:px-4 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="mr-3"
|
||||
<button
|
||||
class="mr-3 p-2 rounded-md hover:bg-background-secondary text-text"
|
||||
on:click={toggleSidebar}
|
||||
aria-label={sidebarVisible ? "Close sidebar" : "Open sidebar"}
|
||||
>
|
||||
@ -24,7 +21,7 @@
|
||||
{:else}
|
||||
<Menu class="h-5 w-5" />
|
||||
{/if}
|
||||
</Button>
|
||||
</button>
|
||||
<div class="text-lg sm:text-xl font-bold text-primary-600">
|
||||
SecureWeb
|
||||
</div>
|
||||
@ -32,9 +29,11 @@
|
||||
|
||||
<div class="flex items-center space-x-2 sm:space-x-4">
|
||||
<!-- Search button for mobile -->
|
||||
<Button variant="ghost" size="icon" class="md:hidden">
|
||||
<button
|
||||
class="md:hidden p-2 rounded-md hover:bg-background-secondary text-text"
|
||||
>
|
||||
<Search class="h-5 w-5" />
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
<!-- Search bar for desktop -->
|
||||
<div class="relative hidden md:block">
|
||||
|
@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { networkService } from "../services/network.service";
|
||||
|
||||
let online = networkService.isOnline();
|
||||
let removeListener: (() => void) | null = null;
|
||||
|
||||
onMount(() => {
|
||||
removeListener = networkService.addStatusChangeListener((status) => {
|
||||
online = status;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (removeListener) {
|
||||
removeListener();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !online}
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 bg-amber-500 text-white py-2 px-4 flex items-center justify-center gap-2 z-50 shadow-[0_-2px_10px_rgba(0,0,0,0.1)] animate-slide-up"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
|
||||
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path>
|
||||
<path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path>
|
||||
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
||||
</svg>
|
||||
<span>You are offline. Some content may not be available.</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
</style>
|
@ -3,7 +3,6 @@
|
||||
import { onMount } from "svelte";
|
||||
import NavItemComponent from "./NavItem.svelte";
|
||||
import { Menu, X } from "lucide-svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
export let navData: NavItem[] = [];
|
||||
export let onNavItemClick: (path: string) => void = () => {};
|
||||
@ -76,11 +75,10 @@
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="bg-background-secondary border-border h-screen fixed top-0 left-0 pt-16 overflow-y-auto shadow-sm z-20
|
||||
class="sidebar bg-background-secondary border-border h-screen fixed top-0 left-0 pt-16 overflow-y-auto shadow-sm z-20
|
||||
{isMobile ? 'w-[85%] max-w-xs' : 'w-64'}
|
||||
transition-transform duration-300 ease-in-out
|
||||
{visible ? 'translate-x-0' : '-translate-x-full'}
|
||||
scrollbar-thin scrollbar-thumb-gray-400/50 scrollbar-track-transparent border-r border-border"
|
||||
{visible ? 'translate-x-0' : '-translate-x-full'}"
|
||||
>
|
||||
<nav class="w-full py-2">
|
||||
{#each navData as item}
|
||||
@ -97,13 +95,32 @@
|
||||
</aside>
|
||||
|
||||
{#if isMobile && !visible}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="fixed top-4 left-4 z-10 shadow-md"
|
||||
<button
|
||||
class="fixed top-4 left-4 z-10 p-2 bg-background-secondary rounded-md shadow-md hover:bg-background"
|
||||
on:click={toggleSidebar}
|
||||
aria-label="Open sidebar"
|
||||
>
|
||||
<Menu class="w-5 h-5" />
|
||||
</Button>
|
||||
<Menu class="w-5 h-5 text-text" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
border-right: 1px solid rgb(var(--color-border));
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<!-- button.svelte -->
|
||||
<script lang="ts">
|
||||
import { cn } from "../../../utils";
|
||||
|
||||
export let variant: "primary" | "secondary" | "ghost" = "primary";
|
||||
export let size: "default" | "sm" | "lg" | "icon" = "default";
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className };
|
||||
export { className as class };
|
||||
|
||||
const sizeClasses = {
|
||||
default: "h-10 px-4 py-2",
|
||||
@ -26,7 +26,6 @@
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
on:click
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
|
@ -1,168 +0,0 @@
|
||||
/**
|
||||
* Content Processing Service
|
||||
* Implements the content processing pipeline as described in the specs
|
||||
* Uses Blake hashing for content integrity and encryption key generation
|
||||
*/
|
||||
|
||||
import { cryptoService } from './crypto.service';
|
||||
import { ipfsService } from './ipfs.service';
|
||||
|
||||
/**
|
||||
* Interface for processed content metadata
|
||||
*/
|
||||
interface ProcessedContentMetadata {
|
||||
originalFilename: string;
|
||||
normalizedFilename: string;
|
||||
blakeHash: string;
|
||||
ipfsCid: string;
|
||||
combinedKey: string;
|
||||
contentType: string;
|
||||
size: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for processing content files
|
||||
* Implements the content processing pipeline:
|
||||
* 1. File discovery
|
||||
* 2. Filename normalization
|
||||
* 3. Original content hashing (Blake)
|
||||
* 4. Content encryption
|
||||
* 5. Encrypted content upload to IPFS
|
||||
*/
|
||||
class ContentProcessorService {
|
||||
/**
|
||||
* Process a file through the content processing pipeline
|
||||
* @param file - File to process
|
||||
* @returns Promise<ProcessedContentMetadata> - Metadata of the processed content
|
||||
*/
|
||||
async processFile(file: File): Promise<ProcessedContentMetadata> {
|
||||
try {
|
||||
// Step 1: Get file information
|
||||
const originalFilename = file.name;
|
||||
|
||||
// Step 2: Normalize filename (lowercase and snake_case)
|
||||
const normalizedFilename = this.normalizeFilename(originalFilename);
|
||||
|
||||
// Step 3: Hash the original content using Blake3
|
||||
const blakeHash = await cryptoService.hashFileBlake3AsHex(file);
|
||||
|
||||
// Step 4: Encrypt the content using the Blake hash as the key
|
||||
// Note: This is a placeholder for actual encryption implementation
|
||||
const encryptedContent = await this.encryptContent(file, blakeHash);
|
||||
|
||||
// Step 5: Upload the encrypted content to IPFS
|
||||
const ipfsCid = await ipfsService.uploadContent(encryptedContent);
|
||||
|
||||
// Create the combined key (Blake hash + IPFS CID)
|
||||
const combinedKey = cryptoService.combineBlakeHashAndIpfsCid(blakeHash, ipfsCid);
|
||||
|
||||
// Return the metadata
|
||||
return {
|
||||
originalFilename,
|
||||
normalizedFilename,
|
||||
blakeHash,
|
||||
ipfsCid,
|
||||
combinedKey,
|
||||
contentType: file.type,
|
||||
size: file.size,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a filename to lowercase and snake_case
|
||||
* @param filename - Original filename
|
||||
* @returns Normalized filename
|
||||
*/
|
||||
normalizeFilename(filename: string): string {
|
||||
// Remove file extension
|
||||
const parts = filename.split('.');
|
||||
const extension = parts.pop() || '';
|
||||
let name = parts.join('.');
|
||||
|
||||
// Convert to lowercase
|
||||
name = name.toLowerCase();
|
||||
|
||||
// Replace spaces and special characters with underscores
|
||||
name = name.replace(/[^a-z0-9]/g, '_');
|
||||
|
||||
// Replace multiple underscores with a single one
|
||||
name = name.replace(/_+/g, '_');
|
||||
|
||||
// Remove leading and trailing underscores
|
||||
name = name.replace(/^_+|_+$/g, '');
|
||||
|
||||
// Add extension back if it exists
|
||||
if (extension) {
|
||||
name = `${name}.${extension.toLowerCase()}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt content using the Blake hash as the key
|
||||
* This is a placeholder for actual encryption implementation
|
||||
* @param file - File to encrypt
|
||||
* @param key - Encryption key (Blake hash)
|
||||
* @returns Promise<string> - Encrypted content as string
|
||||
*/
|
||||
async encryptContent(file: File, key: string): Promise<string> {
|
||||
// TODO: Implement actual encryption
|
||||
// For now, we'll just return a mock encrypted content
|
||||
// This should be replaced with actual encryption using the Blake hash as the key
|
||||
|
||||
// Convert the file to a string
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const contentBytes = new Uint8Array(arrayBuffer);
|
||||
|
||||
// Mock encryption by XORing with the key bytes
|
||||
// This is NOT secure and is only for demonstration purposes
|
||||
const keyBytes = cryptoService.hexToBytes(key);
|
||||
const encryptedBytes = new Uint8Array(contentBytes.length);
|
||||
|
||||
for (let i = 0; i < contentBytes.length; i++) {
|
||||
encryptedBytes[i] = contentBytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
// Convert to base64 for storage
|
||||
return btoa(String.fromCharCode.apply(null, Array.from(encryptedBytes)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt content using the Blake hash as the key
|
||||
* This is a placeholder for actual decryption implementation
|
||||
* @param encryptedContent - Encrypted content as string
|
||||
* @param key - Decryption key (Blake hash)
|
||||
* @returns Promise<Uint8Array> - Decrypted content as Uint8Array
|
||||
*/
|
||||
async decryptContent(encryptedContent: string, key: string): Promise<Uint8Array> {
|
||||
// TODO: Implement actual decryption
|
||||
// For now, we'll just return a mock decrypted content
|
||||
// This should be replaced with actual decryption using the Blake hash as the key
|
||||
|
||||
// Convert the base64 string to bytes
|
||||
const encryptedBytes = new Uint8Array(
|
||||
atob(encryptedContent).split('').map(c => c.charCodeAt(0))
|
||||
);
|
||||
|
||||
// Mock decryption by XORing with the key bytes
|
||||
// This is NOT secure and is only for demonstration purposes
|
||||
const keyBytes = cryptoService.hexToBytes(key);
|
||||
const decryptedBytes = new Uint8Array(encryptedBytes.length);
|
||||
|
||||
for (let i = 0; i < encryptedBytes.length; i++) {
|
||||
decryptedBytes[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
return decryptedBytes;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const contentProcessorService = new ContentProcessorService();
|
@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Cryptography Service
|
||||
* Provides cryptographic functions for content processing
|
||||
* Implements Blake hashing algorithm for content integrity and encryption key generation
|
||||
*/
|
||||
|
||||
import { blake3 } from '@noble/hashes/blake3';
|
||||
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
|
||||
|
||||
/**
|
||||
* Service for cryptographic operations
|
||||
* Provides methods for hashing, encryption, and decryption
|
||||
*/
|
||||
class CryptoService {
|
||||
/**
|
||||
* Hash content using Blake3 algorithm
|
||||
* @param content - Content to hash as Uint8Array
|
||||
* @returns Blake3 hash as Uint8Array
|
||||
*/
|
||||
hashBlake3(content: Uint8Array): Uint8Array {
|
||||
return blake3(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash content using Blake3 algorithm with custom output length
|
||||
* @param content - Content to hash as Uint8Array
|
||||
* @param outputLength - Length of the output hash in bytes
|
||||
* @returns Blake3 hash as Uint8Array
|
||||
*/
|
||||
hashBlake3WithLength(content: Uint8Array, outputLength: number): Uint8Array {
|
||||
return blake3(content, { dkLen: outputLength });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash string content using Blake3 algorithm
|
||||
* @param content - Content to hash as string
|
||||
* @returns Blake3 hash as Uint8Array
|
||||
*/
|
||||
hashStringBlake3(content: string): Uint8Array {
|
||||
const contentBytes = utf8ToBytes(content);
|
||||
return this.hashBlake3(contentBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash string content using Blake3 algorithm and return as hex string
|
||||
* @param content - Content to hash as string
|
||||
* @returns Blake3 hash as hex string
|
||||
*/
|
||||
hashStringBlake3AsHex(content: string): string {
|
||||
const hash = this.hashStringBlake3(content);
|
||||
return bytesToHex(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash file content using Blake3 algorithm
|
||||
* @param file - File to hash
|
||||
* @returns Promise<Uint8Array> - Blake3 hash as Uint8Array
|
||||
*/
|
||||
async hashFileBlake3(file: File): Promise<Uint8Array> {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const contentBytes = new Uint8Array(buffer);
|
||||
return this.hashBlake3(contentBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash file content using Blake3 algorithm and return as hex string
|
||||
* @param file - File to hash
|
||||
* @returns Promise<string> - Blake3 hash as hex string
|
||||
*/
|
||||
async hashFileBlake3AsHex(file: File): Promise<string> {
|
||||
const hash = await this.hashFileBlake3(file);
|
||||
return bytesToHex(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine Blake hash and IPFS CID into a single key string
|
||||
* Format: <blake_hash>:<ipfs_cid>
|
||||
* @param blakeHash - Blake hash as hex string
|
||||
* @param ipfsCid - IPFS CID as string
|
||||
* @returns Combined key string
|
||||
*/
|
||||
combineBlakeHashAndIpfsCid(blakeHash: string, ipfsCid: string): string {
|
||||
return `${blakeHash}:${ipfsCid}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split combined key string into Blake hash and IPFS CID
|
||||
* @param combinedKey - Combined key string in format <blake_hash>:<ipfs_cid>
|
||||
* @returns Object with blakeHash and ipfsCid
|
||||
*/
|
||||
splitCombinedKey(combinedKey: string): { blakeHash: string; ipfsCid: string } {
|
||||
const [blakeHash, ipfsCid] = combinedKey.split(':');
|
||||
return { blakeHash, ipfsCid };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string to Uint8Array
|
||||
* @param hex - Hex string
|
||||
* @returns Uint8Array
|
||||
*/
|
||||
hexToBytes(hex: string): Uint8Array {
|
||||
return hexToBytes(hex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Uint8Array to hex string
|
||||
* @param bytes - Uint8Array
|
||||
* @returns Hex string
|
||||
*/
|
||||
bytesToHex(bytes: Uint8Array): string {
|
||||
return bytesToHex(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to Uint8Array using UTF-8 encoding
|
||||
* @param str - String to convert
|
||||
* @returns Uint8Array
|
||||
*/
|
||||
stringToBytes(str: string): Uint8Array {
|
||||
return utf8ToBytes(str);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const cryptoService = new CryptoService();
|
@ -1,309 +0,0 @@
|
||||
import { openDB } from 'idb'
|
||||
import type { DBSchema, IDBPDatabase } from 'idb'
|
||||
|
||||
/**
|
||||
* Interface for the IPFS cache database schema
|
||||
*/
|
||||
interface IPFSCacheDB extends DBSchema {
|
||||
'ipfs-content': {
|
||||
key: string;
|
||||
value: {
|
||||
cid: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
contentType: string;
|
||||
};
|
||||
indexes: { 'by-timestamp': number };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for caching IPFS content locally using IndexedDB
|
||||
* Provides methods for storing and retrieving content from the cache
|
||||
*/
|
||||
class IPFSCacheService {
|
||||
private db: IDBPDatabase<IPFSCacheDB> | null = null
|
||||
private readonly DB_NAME = 'ipfs-cache'
|
||||
private readonly STORE_NAME = 'ipfs-content'
|
||||
private readonly MAX_CACHE_SIZE = 50 * 1024 * 1024 // 50MB
|
||||
private readonly MAX_CACHE_AGE = 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
|
||||
/**
|
||||
* Initialize the cache database
|
||||
* @returns Promise<boolean> - True if initialization was successful
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
try {
|
||||
this.db = await openDB<IPFSCacheDB>(this.DB_NAME, 1, {
|
||||
upgrade(db) {
|
||||
const store = db.createObjectStore('ipfs-content', { keyPath: 'cid' })
|
||||
store.createIndex('by-timestamp', 'timestamp')
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up old cache entries on initialization
|
||||
await this.cleanupCache()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize IPFS cache:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache text content
|
||||
* @param cid - Content identifier
|
||||
* @param content - Content to cache
|
||||
* @param contentType - MIME type of the content
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async cacheContent(cid: string, content: string, contentType: string = 'text/plain'): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.put(this.STORE_NAME, {
|
||||
cid,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
contentType
|
||||
})
|
||||
|
||||
// Check cache size and clean up if necessary
|
||||
await this.checkCacheSize()
|
||||
} catch (error) {
|
||||
console.error(`Failed to cache content for CID ${cid}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache binary content as a base64 string
|
||||
* @param cid - Content identifier
|
||||
* @param blob - Binary content to cache
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async cacheBlob(cid: string, blob: Blob): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert blob to base64 string for storage
|
||||
const reader = new FileReader()
|
||||
const contentPromise = new Promise<string>((resolve, reject) => {
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = reject
|
||||
})
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
const content = await contentPromise
|
||||
|
||||
await this.db.put(this.STORE_NAME, {
|
||||
cid,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
contentType: blob.type || 'application/octet-stream'
|
||||
})
|
||||
|
||||
// Check cache size and clean up if necessary
|
||||
await this.checkCacheSize()
|
||||
} catch (error) {
|
||||
console.error(`Failed to cache blob for CID ${cid}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text content from cache
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<string | null> - Cached content or null if not found
|
||||
*/
|
||||
async getContent(cid: string): Promise<string | null> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await this.db.get(this.STORE_NAME, cid)
|
||||
|
||||
if (!entry || entry.contentType !== 'text/plain') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update timestamp to mark as recently used
|
||||
await this.db.put(this.STORE_NAME, {
|
||||
...entry,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
return entry.content
|
||||
} catch (error) {
|
||||
console.error(`Failed to get cached content for CID ${cid}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get binary content from cache
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<Blob | null> - Cached content as Blob or null if not found
|
||||
*/
|
||||
async getBlob(cid: string): Promise<Blob | null> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await this.db.get(this.STORE_NAME, cid)
|
||||
|
||||
if (!entry || entry.contentType === 'text/plain') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update timestamp to mark as recently used
|
||||
await this.db.put(this.STORE_NAME, {
|
||||
...entry,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// Convert base64 string back to blob
|
||||
const response = await fetch(entry.content)
|
||||
return await response.blob()
|
||||
} catch (error) {
|
||||
console.error(`Failed to get cached blob for CID ${cid}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content is in cache
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<boolean> - True if content is in cache
|
||||
*/
|
||||
async hasCached(cid: string): Promise<boolean> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await this.db.get(this.STORE_NAME, cid)
|
||||
return !!entry
|
||||
} catch (error) {
|
||||
console.error(`Failed to check cache for CID ${cid}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove content from cache
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async removeFromCache(cid: string): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.delete(this.STORE_NAME, cid)
|
||||
} catch (error) {
|
||||
console.error(`Failed to remove CID ${cid} from cache:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire cache
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async clearCache(): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error('Cache not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.clear(this.STORE_NAME)
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old cache entries
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
private async cleanupCache(): Promise<void> {
|
||||
if (!this.db) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const now = Date.now()
|
||||
const expiredTimestamp = now - this.MAX_CACHE_AGE
|
||||
|
||||
// Get all entries older than MAX_CACHE_AGE
|
||||
const tx = this.db.transaction(this.STORE_NAME, 'readwrite')
|
||||
const index = tx.store.index('by-timestamp')
|
||||
let cursor = await index.openCursor(IDBKeyRange.upperBound(expiredTimestamp))
|
||||
|
||||
// Delete expired entries
|
||||
while (cursor) {
|
||||
await cursor.delete()
|
||||
cursor = await cursor.continue()
|
||||
}
|
||||
|
||||
await tx.done
|
||||
} catch (error) {
|
||||
console.error('Failed to clean up cache:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check cache size and remove oldest entries if necessary
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
private async checkCacheSize(): Promise<void> {
|
||||
if (!this.db) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all entries
|
||||
const entries = await this.db.getAll(this.STORE_NAME)
|
||||
|
||||
// Calculate total size
|
||||
let totalSize = 0
|
||||
for (const entry of entries) {
|
||||
totalSize += entry.content.length
|
||||
}
|
||||
|
||||
// If cache is too large, remove oldest entries
|
||||
if (totalSize > this.MAX_CACHE_SIZE) {
|
||||
// Sort by timestamp (oldest first)
|
||||
entries.sort((a, b) => a.timestamp - b.timestamp)
|
||||
|
||||
// Remove oldest entries until we're under the limit
|
||||
const tx = this.db.transaction(this.STORE_NAME, 'readwrite')
|
||||
for (const entry of entries) {
|
||||
await tx.store.delete(entry.cid)
|
||||
totalSize -= entry.content.length
|
||||
|
||||
if (totalSize <= this.MAX_CACHE_SIZE * 0.8) { // Reduce to 80% of max
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await tx.done
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check cache size:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const ipfsCacheService = new IPFSCacheService()
|
@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Service for fetching IPFS content through public gateways
|
||||
* Provides fallback when direct IPFS connections fail
|
||||
*/
|
||||
class IPFSGatewayService {
|
||||
// List of public IPFS gateways to try
|
||||
private gateways = [
|
||||
'https://ipfs.io/ipfs/',
|
||||
'https://gateway.pinata.cloud/ipfs/',
|
||||
'https://cloudflare-ipfs.com/ipfs/',
|
||||
'https://dweb.link/ipfs/'
|
||||
]
|
||||
|
||||
/**
|
||||
* Fetch content from a gateway by trying each gateway in order until one succeeds
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<Response> - Response from the gateway
|
||||
*/
|
||||
async fetchFromGateway(cid: string): Promise<Response> {
|
||||
// Try gateways in order until one succeeds
|
||||
for (const gateway of this.gateways) {
|
||||
try {
|
||||
const response = await fetch(`${gateway}${cid}`)
|
||||
if (response.ok) {
|
||||
return response
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Gateway ${gateway} failed for CID ${cid}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`All gateways failed for CID ${cid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content as text from a gateway
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<string> - Content as string
|
||||
*/
|
||||
async getContent(cid: string): Promise<string> {
|
||||
const response = await this.fetchFromGateway(cid)
|
||||
return await response.text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content as blob from a gateway
|
||||
* @param cid - Content identifier
|
||||
* @returns Promise<Blob> - Content as blob
|
||||
*/
|
||||
async getImage(cid: string): Promise<Blob> {
|
||||
const response = await this.fetchFromGateway(cid)
|
||||
return await response.blob()
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const ipfsGatewayService = new IPFSGatewayService()
|
@ -1,324 +0,0 @@
|
||||
import { createHelia } from 'helia'
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
import type { Helia } from '@helia/interface'
|
||||
import type { UnixFS } from '@helia/unixfs'
|
||||
import { CID } from 'multiformats/cid'
|
||||
import { createLibp2p } from 'libp2p'
|
||||
import { webSockets } from '@libp2p/websockets'
|
||||
import { webRTC } from '@libp2p/webrtc'
|
||||
import { webTransport } from '@libp2p/webtransport'
|
||||
import { ipfsGatewayService } from './ipfs-gateway.service'
|
||||
import { ipfsCacheService } from './ipfs-cache.service'
|
||||
import { networkService } from './network.service'
|
||||
|
||||
/**
|
||||
* Service for interacting with IPFS using Helia
|
||||
* Provides methods for initializing IPFS, retrieving content, and uploading content
|
||||
*/
|
||||
class IPFSService {
|
||||
private helia: Helia | null = null
|
||||
private fs: UnixFS | null = null
|
||||
|
||||
/**
|
||||
* Initialize the IPFS client
|
||||
* @returns Promise<boolean> - True if initialization was successful
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
try {
|
||||
// Initialize cache first
|
||||
await ipfsCacheService.initialize()
|
||||
|
||||
console.log('Initializing IPFS with simplified configuration...')
|
||||
|
||||
// Create a Helia instance with minimal configuration
|
||||
this.helia = await createHelia({
|
||||
// Use minimal configuration for browser environment
|
||||
})
|
||||
|
||||
console.log('Helia instance created successfully')
|
||||
|
||||
// Create a UnixFS instance for file operations
|
||||
if (this.helia) {
|
||||
this.fs = unixfs(this.helia)
|
||||
console.log('UnixFS instance created successfully')
|
||||
}
|
||||
|
||||
console.log('IPFS initialized successfully')
|
||||
return true
|
||||
} catch (error) {
|
||||
// More detailed error logging
|
||||
console.error('Failed to initialize IPFS:', error)
|
||||
if (error instanceof Error) {
|
||||
console.error('Error message:', error.message)
|
||||
console.error('Error stack:', error.stack)
|
||||
}
|
||||
|
||||
// For demo purposes, return true to allow the app to function
|
||||
// In a production environment, you would handle this differently
|
||||
console.warn('Continuing with IPFS in mock mode for demo purposes')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content from IPFS by CID
|
||||
* @param cidStr - Content identifier as string
|
||||
* @returns Promise<string> - Content as string
|
||||
*/
|
||||
async getContent(cidStr: string): Promise<string> {
|
||||
// Check cache first
|
||||
try {
|
||||
const cachedContent = await ipfsCacheService.getContent(cidStr)
|
||||
if (cachedContent) {
|
||||
console.log(`Retrieved content for CID ${cidStr} from cache`)
|
||||
return cachedContent
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Cache retrieval failed for CID ${cidStr}:`, error)
|
||||
}
|
||||
|
||||
// If offline and not in cache, throw error
|
||||
if (!networkService.isOnline()) {
|
||||
throw new Error('Cannot retrieve content: You are offline and the content is not available in the cache')
|
||||
}
|
||||
|
||||
// For demo purposes, if IPFS is not initialized, use mock data
|
||||
if (!this.fs) {
|
||||
console.warn('IPFS not initialized, using mock data for demo')
|
||||
return `# Mock IPFS Content\n\nThis is mock content for CID: ${cidStr}\n\nIPFS is not fully initialized, but the demo can still show the basic functionality.`
|
||||
}
|
||||
|
||||
try {
|
||||
const cid = CID.parse(cidStr)
|
||||
// Fetch content from IPFS
|
||||
const decoder = new TextDecoder()
|
||||
let content = ''
|
||||
|
||||
for await (const chunk of this.fs.cat(cid)) {
|
||||
content += decoder.decode(chunk, { stream: true })
|
||||
}
|
||||
|
||||
// Cache the content
|
||||
try {
|
||||
await ipfsCacheService.cacheContent(cidStr, content, 'text/plain')
|
||||
} catch (cacheError) {
|
||||
console.warn(`Failed to cache content for CID ${cidStr}:`, cacheError)
|
||||
}
|
||||
|
||||
return content
|
||||
} catch (error) {
|
||||
console.warn(`Direct IPFS retrieval failed for CID ${cidStr}, trying gateways:`, error)
|
||||
|
||||
try {
|
||||
// Try gateway fallback
|
||||
const content = await ipfsGatewayService.getContent(cidStr)
|
||||
|
||||
// Cache the content
|
||||
try {
|
||||
await ipfsCacheService.cacheContent(cidStr, content, 'text/plain')
|
||||
} catch (cacheError) {
|
||||
console.warn(`Failed to cache content from gateway for CID ${cidStr}:`, cacheError)
|
||||
}
|
||||
|
||||
return content
|
||||
} catch (error: any) {
|
||||
const gatewayError = error instanceof Error ? error : new Error(String(error))
|
||||
console.error(`Gateway fallback also failed for CID ${cidStr}:`, gatewayError)
|
||||
|
||||
// For demo purposes, return mock data
|
||||
return `# Mock IPFS Content\n\nThis is mock content for CID: ${cidStr}\n\nFailed to retrieve actual content, but the demo can still show the basic functionality.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image data from IPFS by CID
|
||||
* @param cidStr - Content identifier as string
|
||||
* @returns Promise<Blob> - Image as Blob
|
||||
*/
|
||||
async getImage(cidStr: string): Promise<Blob> {
|
||||
// Check cache first
|
||||
try {
|
||||
const cachedBlob = await ipfsCacheService.getBlob(cidStr)
|
||||
if (cachedBlob) {
|
||||
console.log(`Retrieved image for CID ${cidStr} from cache`)
|
||||
return cachedBlob
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Cache retrieval failed for CID ${cidStr}:`, error)
|
||||
}
|
||||
|
||||
// If offline and not in cache, throw error
|
||||
if (!networkService.isOnline()) {
|
||||
throw new Error('Cannot retrieve image: You are offline and the image is not available in the cache')
|
||||
}
|
||||
|
||||
// For demo purposes, if IPFS is not initialized, use a placeholder image
|
||||
if (!this.fs) {
|
||||
console.warn('IPFS not initialized, using placeholder image for demo')
|
||||
// Create a simple SVG as a placeholder
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
|
||||
<rect width="200" height="200" fill="#f0f0f0"/>
|
||||
<text x="50%" y="50%" font-family="Arial" font-size="16" text-anchor="middle">Placeholder Image</text>
|
||||
<text x="50%" y="70%" font-family="Arial" font-size="12" text-anchor="middle">CID: ${cidStr}</text>
|
||||
</svg>`;
|
||||
return new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
}
|
||||
|
||||
try {
|
||||
const cid = CID.parse(cidStr)
|
||||
// Fetch image data from IPFS
|
||||
const chunks: Uint8Array[] = []
|
||||
|
||||
for await (const chunk of this.fs.cat(cid)) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Combine chunks into a single Uint8Array
|
||||
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0)
|
||||
const allChunks = new Uint8Array(totalLength)
|
||||
|
||||
let offset = 0
|
||||
for (const chunk of chunks) {
|
||||
allChunks.set(chunk, offset)
|
||||
offset += chunk.length
|
||||
}
|
||||
|
||||
// Create a Blob from the Uint8Array
|
||||
const blob = new Blob([allChunks])
|
||||
|
||||
// Cache the blob
|
||||
try {
|
||||
await ipfsCacheService.cacheBlob(cidStr, blob)
|
||||
} catch (cacheError) {
|
||||
console.warn(`Failed to cache image for CID ${cidStr}:`, cacheError)
|
||||
}
|
||||
|
||||
return blob
|
||||
} catch (error) {
|
||||
console.warn(`Direct IPFS image retrieval failed for CID ${cidStr}, trying gateways:`, error)
|
||||
|
||||
try {
|
||||
// Try gateway fallback
|
||||
const blob = await ipfsGatewayService.getImage(cidStr)
|
||||
|
||||
// Cache the blob
|
||||
try {
|
||||
await ipfsCacheService.cacheBlob(cidStr, blob)
|
||||
} catch (cacheError) {
|
||||
console.warn(`Failed to cache image from gateway for CID ${cidStr}:`, cacheError)
|
||||
}
|
||||
|
||||
return blob
|
||||
} catch (error: any) {
|
||||
const gatewayError = error instanceof Error ? error : new Error(String(error))
|
||||
console.error(`Gateway fallback also failed for CID ${cidStr}:`, gatewayError)
|
||||
|
||||
// For demo purposes, return a placeholder image
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
|
||||
<rect width="200" height="200" fill="#ffeeee"/>
|
||||
<text x="50%" y="50%" font-family="Arial" font-size="16" text-anchor="middle" fill="red">Image Not Found</text>
|
||||
<text x="50%" y="70%" font-family="Arial" font-size="12" text-anchor="middle">CID: ${cidStr}</text>
|
||||
</svg>`;
|
||||
return new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload content to IPFS
|
||||
* @param content - Content as string
|
||||
* @returns Promise<string> - CID of the uploaded content
|
||||
*/
|
||||
async uploadContent(content: string): Promise<string> {
|
||||
if (!this.fs) {
|
||||
console.warn('IPFS not initialized, using mock CID for demo')
|
||||
// Generate a mock CID for demo purposes
|
||||
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
|
||||
|
||||
// Cache the content with the mock CID
|
||||
try {
|
||||
await ipfsCacheService.cacheContent(mockCid, content, 'text/plain')
|
||||
} catch (error) {
|
||||
console.warn('Failed to cache mock content:', error)
|
||||
}
|
||||
|
||||
return mockCid;
|
||||
}
|
||||
|
||||
try {
|
||||
const encoder = new TextEncoder()
|
||||
const cid = await this.fs.addBytes(encoder.encode(content))
|
||||
return cid.toString()
|
||||
} catch (error) {
|
||||
console.error('Failed to upload content:', error)
|
||||
|
||||
// For demo purposes, return a mock CID
|
||||
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
|
||||
|
||||
// Cache the content with the mock CID
|
||||
try {
|
||||
await ipfsCacheService.cacheContent(mockCid, content, 'text/plain')
|
||||
} catch (cacheError) {
|
||||
console.warn('Failed to cache mock content:', cacheError)
|
||||
}
|
||||
|
||||
return mockCid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to IPFS
|
||||
* @param file - File to upload
|
||||
* @returns Promise<string> - CID of the uploaded file
|
||||
*/
|
||||
async uploadFile(file: File): Promise<string> {
|
||||
if (!this.fs) {
|
||||
console.warn('IPFS not initialized, using mock CID for demo')
|
||||
// Generate a mock CID for demo purposes
|
||||
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
|
||||
|
||||
// Cache the file with the mock CID
|
||||
try {
|
||||
const blob = new Blob([await file.arrayBuffer()], { type: file.type });
|
||||
await ipfsCacheService.cacheBlob(mockCid, blob)
|
||||
} catch (error) {
|
||||
console.warn('Failed to cache mock file:', error)
|
||||
}
|
||||
|
||||
return mockCid;
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const cid = await this.fs.addBytes(new Uint8Array(buffer))
|
||||
return cid.toString()
|
||||
} catch (error) {
|
||||
console.error('Failed to upload file:', error)
|
||||
|
||||
// For demo purposes, return a mock CID
|
||||
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
|
||||
|
||||
// Cache the file with the mock CID
|
||||
try {
|
||||
const blob = new Blob([await file.arrayBuffer()], { type: file.type });
|
||||
await ipfsCacheService.cacheBlob(mockCid, blob)
|
||||
} catch (cacheError) {
|
||||
console.warn('Failed to cache mock file:', cacheError)
|
||||
}
|
||||
|
||||
return mockCid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IPFS is initialized
|
||||
* @returns boolean - True if IPFS is initialized
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.helia !== null && this.fs !== null
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const ipfsService = new IPFSService()
|
@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Service for monitoring network status
|
||||
* Provides methods to check if the user is online and to register listeners for network status changes
|
||||
*/
|
||||
class NetworkService {
|
||||
private online: boolean = navigator.onLine
|
||||
private listeners: Set<(online: boolean) => void> = new Set()
|
||||
|
||||
constructor() {
|
||||
// Initialize with current online status
|
||||
this.online = navigator.onLine
|
||||
|
||||
// Add event listeners for online/offline events
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('online', this.handleOnline.bind(this))
|
||||
window.addEventListener('offline', this.handleOffline.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is currently online
|
||||
* @returns boolean - True if online, false if offline
|
||||
*/
|
||||
isOnline(): boolean {
|
||||
return this.online
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for network status changes
|
||||
* @param listener - Function to call when network status changes
|
||||
* @returns Function - Function to remove the listener
|
||||
*/
|
||||
addStatusChangeListener(listener: (online: boolean) => void): () => void {
|
||||
this.listeners.add(listener)
|
||||
|
||||
// Return function to remove listener
|
||||
return () => {
|
||||
this.listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle online event
|
||||
*/
|
||||
private handleOnline(): void {
|
||||
this.online = true
|
||||
this.notifyListeners()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle offline event
|
||||
*/
|
||||
private handleOffline(): void {
|
||||
this.online = false
|
||||
this.notifyListeners()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners of network status change
|
||||
*/
|
||||
private notifyListeners(): void {
|
||||
for (const listener of this.listeners) {
|
||||
listener(this.online)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const networkService = new NetworkService()
|
Loading…
Reference in New Issue
Block a user