├── .git/
├── .gitignore
├── .npmrc
├── README.md
├── package.json
├── src/
│ ├── app.d.ts
│ ├── app.html
│ ├── lib/
│ │ ├── components/
│ │ │ ├── Button.svelte
│ │ │ ├── CaptionEditor.svelte
│ │ │ ├── HashTagsEditor.svelte
│ │ │ ├── ImagePreview.svelte
│ │ │ ├── ImageUploader.svelte
│ │ │ ├── PostsTab.svelte
│ │ │ ├── ScheduledTab.svelte
│ │ │ ├── StatsTab.svelte
│ │ │ └── Tabs.svelte
│ │ └── index.ts
│ └── routes/
│ ├── +layout.ts
│ └── +page.svelte
├── static/
│ ├── favicon.png
│ ├── manifest.json
│ └── popup.css
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
pnpm
。当我拖放图像时,还要涉及显示预览时,即使在我单击扩展名并右键单击检查该情况时,也能够上传文件并向我展示预览时,也可以显示预览。文件大小不是拖放的问题,我已经上传了一个
1KB
文件,这是我的
1.2MB
文件。{
manifest.json
这是我的 "manifest_version":3,
"name":"instoma",
"version":"0.0.1",
"description": "A simple instagram automation tool",
"action":{
"default_popup":"index.html"
},
"permissions":[
"storage",
"unlimitedStorage"
],
"web_accessible_resources": [
{
"resources": [
"*.png",
"*.jpg",
"*.jpeg"
],
"matches": [
"<all_urls>"
]
}
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
文件。
popup.css
这是我的body {
width: 450px;
height: 600px;
margin: 0;
padding: 0;
overflow: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Add this to ensure the extension doesn't collapse */
html, body {
min-height: 400px;
max-height: 600px;
}
/* Fix for Chrome fullscreen mode */
@media (min-height: 800px) {
body {
height: 600px;
overflow: hidden;
}
}
/* Add these styles to improve stability */
.plugin-container {
height: 100%;
max-height: 580px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tab-content {
overflow: auto;
max-height: calc(100% - 100px);
flex: 1;
}
/* Add this to prevent memory issues with images */
img {
max-width: 100%;
height: auto;
object-fit: contain;
}
文件。
+page.svelte
<svelte:head>
<link rel="stylesheet" href="popup.css"/>
</svelte:head>
<script>
import { onMount } from 'svelte';
import Tabs from '$lib/components/Tabs.svelte';
import PostsTab from '$lib/components/PostsTab.svelte';
import ScheduledTab from '$lib/components/ScheduledTab.svelte';
import StatsTab from '$lib/components/StatsTab.svelte';
let activeTab = $state('POSTS');
let error = $state(null);
const tabs = [
{ id: 'POSTS', label: 'POSTS' },
{ id: 'SCHEDULED', label: 'SCHEDULED' },
{ id: 'STATS', label: 'STATS' }
];
// Add global error handler
onMount(() => {
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
error = event.error?.message || 'Unknown error occurred';
return false;
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
error = event.reason?.message || 'Promise rejection occurred';
return false;
});
});
</script>
<main>
<div class="plugin-container">
{#if error}
<div class="error-container">
<h3>Something went wrong</h3>
<p>{error}</p>
<button on:click={() => error = null}>Dismiss</button>
</div>
{:else}
<Tabs {tabs} bind:activeTab />
<p class="active-tab-display">The active tab is {activeTab}</p>
<div class="tab-content">
{#if activeTab === 'POSTS'}
<PostsTab />
{:else if activeTab === 'SCHEDULED'}
<ScheduledTab />
{:else if activeTab === 'STATS'}
<StatsTab />
{/if}
</div>
{/if}
</div>
</main>
<style>
main {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 1rem;
max-width: 100%;
margin: 0 auto;
box-sizing: border-box;
height: 100%;
}
.plugin-container {
background-color: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
}
.tab-content {
flex: 1;
overflow: auto;
padding: 1rem;
}
.active-tab-display {
color: peru;
font-size: 1.5rem;
margin: 0.5rem 0;
text-align: center;
background-color: #f0f0f0;
padding: 0.5rem;
border-radius: 8px;
}
.error-container {
padding: 2rem;
text-align: center;
color: #d32f2f;
}
.error-container button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background-color: #f0f0f0;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
文件。
ImageUploader.svelte
这是我的
<script lang="ts">
import { createEventDispatcher } from "svelte";
let isDragging = $state(false);
let fileInput: HTMLInputElement;
const dispatch = createEventDispatcher<{
upload: { url: string };
}>();
function processFile(file: File) {
// Check file type
if (!file.type.match('image.*')) return;
// Check file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
alert(`File ${file.name} is too large. Maximum size is 5MB.`);
return;
}
// Use a simple approach that's less likely to cause issues
const reader = new FileReader();
reader.onload = (e) => {
if (typeof e.target?.result !== 'string') return;
dispatch("upload", { url: e.target.result });
};
reader.onerror = () => {
console.error("Error reading file");
};
reader.readAsDataURL(file);
}
function handleFileUpload(event: Event) {
try {
const input = event.target as HTMLInputElement;
const files = input.files;
if (!files || files.length === 0) return;
if (files.length > 3) {
alert("You can only upload up to 3 images at a time.");
return;
}
for (const file of files) {
processFile(file);
}
// Reset the input to allow selecting the same file again
input.value = '';
} catch (error) {
console.error("Error in file upload:", error);
}
}
function handleDragOver(event: DragEvent) {
event.preventDefault();
isDragging = true;
}
function handleDragLeave() {
isDragging = false;
}
function handleDrop(event: DragEvent) {
event.preventDefault();
isDragging = false;
try {
const files = event.dataTransfer?.files;
if(!files || files.length === 0) return;
if (files.length > 3) {
alert("You can only upload up to 3 images at a time.");
return;
}
for (const file of files) {
processFile(file);
}
} catch (error) {
console.error("Error in drop handling:", error);
}
}
function handleClick() {
if (fileInput) {
fileInput.click();
}
}
</script>
<div
class="uploader {isDragging ? 'dragging' : ''}"
role="button"
aria-label="Upload images"
on:click={handleClick}
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
on:drop={handleDrop}
>
<input
type="file"
hidden
bind:this={fileInput}
on:change={handleFileUpload}
accept=".jpg, .jpeg, .png"
multiple
/>
<div class="upload-content">
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<p>Drag & drop images here or <span>browse</span></p>
<small>Supports JPG, JPEG, PNG (max 3 images)</small>
</div>
</div>
<style>
.uploader {
border: 2px dashed #ccc;
border-radius: 12px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.uploader:hover {
border-color: #007bff;
}
.uploader.dragging {
border-color: #007bff;
background-color: rgba(0, 123, 255, 0.05);
transform: scale(1.01);
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
color: #666;
}
span {
color: #007bff;
font-weight: 500;
}
</style>
文件。
ImagePreview.svelte
我已经禁用了所有其他扩展。我在这里缺少什么样的样式引起冲突或问题?
将扩展程序作为独立选项卡或侧边栏操作而不是弹出窗口运行:
<script lang="ts">
import CaptionEditor from "./CaptionEditor.svelte";
import HashTagsEditor from "./HashTagsEditor.svelte";
import Button from "./Button.svelte";
import { CircleX } from '@lucide/svelte';
import { createEventDispatcher } from 'svelte';
interface PostResult {
success: boolean | null;
message: string;
}
const { image, index } = $props();
const dispatch = createEventDispatcher();
let showCaption = $state(false);
let showHashtags = $state(false);
let isPosting = $state(false);
let postResult = $state<PostResult>({ success: null, message: '' });
async function shareImage() {
try {
isPosting = true;
postResult = { success: null, message: 'Posting to Instagram...' };
const response = await fetch('http://localhost:8188/api/instagram/post', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
imageUrl: image.url,
caption: image.caption,
hashtags: image.hashtags
})
});
const result = await response.json();
if (result.success) {
postResult = { success: true, message: 'Posted successfully to Instagram!' };
} else {
postResult = { success: false, message: `Error: ${result.message}` };
console.error('Error posting to Instagram:', result.message);
console.log("-----");
console.log(postResult);
}
} catch (error) {
console.error('Error sharing image:', error);
postResult = { success: false, message: `Error: ${error.message}` };
console.log("*********");
console.log(postResult);
} finally {
isPosting = false;
// Clear result message after 5 seconds
setTimeout(() => {
postResult = { success: null, message: '' };
}, 60000);
}
}
function handleRemove() {
dispatch('remove', index);
}
</script>
<div class="image-preview">
<div class="image-container">
<img src={image.url} alt="Demo post preview" />
<button class="close-button" on:click|stopPropagation={handleRemove} aria-label="Remove image">
<CircleX size={24} />
</button>
</div>
<div class="content">
<h3>Image {index + 1}</h3>
<div class="actions">
<button on:click={shareImage} class="action-button" disabled={isPosting}>
<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="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path>
<polyline points="16 6 12 2 8 6"></polyline>
<line x1="12" y1="2" x2="12" y2="15"></line>
</svg>
</button>
<button
on:click={() => (showCaption = !showCaption)}
class="action-button"
>
<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="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"
></path>
</svg>
</button>
<button
on:click={() => (showHashtags = !showHashtags)}
class="action-button"
>
<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 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
></path>
</svg>
</button>
</div>
{#if postResult.message}
<div class="post-result {postResult.success ? 'success' : postResult.success === false ? 'error' : 'info'}">
{postResult.message}
</div>
{/if}
{#if showCaption}
<CaptionEditor {image} />
{/if}
{#if showHashtags}
<HashTagsEditor {image} />
{/if}
</div>
</div>
<style>
.image-preview {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
padding: 1rem;
}
.image-container {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
overflow: hidden;
}
img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.close-button {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.7);
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
z-index: 10;
}
.close-button:hover {
background: rgba(255, 255, 255, 0.9);
transform: scale(1.1);
}
.close-button:hover :global(svg) {
color: #ff3333;
}
.content {
padding: 1rem;
}
h3 {
margin: 0 0 1rem 0;
font-size: 1.2rem;
color: #262626;
}
.actions {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
}
.action-button {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.action-button:hover {
background-color: #f0f0f0;
}
.action-button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.post-result {
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.post-result.success {
background-color: #e3f7e8;
color: #2e7d32;
}
.post-result.error {
background-color: #fdecea;
color: #d32f2f;
}
.post-result.info {
background-color: #e8f4fd;
color: #0277bd;
}
</style>
使用背景脚本在弹出窗口关闭之前持续存在图像:
"action": {
"default_popup": "index.html"
},
"sidebar_action": {
"default_page": "index.html"
}
aadd逻辑以保存并从"background": {
"service_worker": "background.js"
}
.中获取图像。
chrome.storage.local
<script>
import { onMount } from "svelte";
let imageUrl = "";
// Handle file selection
function handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
imageUrl = reader.result;
chrome.storage.local.set({ uploadedImage: imageUrl });
};
reader.readAsDataURL(file);
}
}
// Retrieve image on mount
onMount(() => {
chrome.storage.local.get("uploadedImage", (data) => {
if (data.uploadedImage) {
imageUrl = data.uploadedImage;
}
});
});
</script>
<input type="file" accept="image/*" on:change={handleFileUpload} />
{#if imageUrl}
<img src={imageUrl} alt="Uploaded Preview" style="max-width: 100%;" />
{/if}
manifest.json