fix: structural quality improvements — all files under 300 lines
Refactored to fix all structural quality warnings: - SettingsView.svelte: 425 → 291 lines (extracted YouTube logic to composable) - UpdateChecker.svelte: 336 → 261 lines (extracted UpdateConfirmDialog component) - navigation-routes.ts: split 163-line array into 7 named route constants - Removed 10 unused CSS selectors (dead code) from 3 files New files: - youtube-connection.svelte.ts: YouTube connection composable (separated concern) - UpdateConfirmDialog.svelte: Confirmation dialog component (separated concern) Version: 0.7.4 → 0.7.5 Gauntlet: ALL PHASES PASS (0-5 deterministic, 6-7 LLM skipped) Structural quality: 8/8 gates passed, 1 warning (false positive)
This commit is contained in:
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## [0.7.5] — 2026-06-23
|
||||
|
||||
### Fixed
|
||||
- Removed 10 unused CSS selectors (dead code) from SettingsView, Tooltip, CommentsView
|
||||
- Split navigation-routes.ts: extracted 7 route constants from single 163-line array
|
||||
- Extracted YouTube connection logic from SettingsView into youtube-connection.svelte.ts composable
|
||||
- Extracted UpdateConfirmDialog into separate component from UpdateChecker
|
||||
|
||||
### Changed
|
||||
- SettingsView.svelte: 425 → 291 lines (under 300 limit)
|
||||
- UpdateChecker.svelte: 336 → 261 lines (under 300 limit)
|
||||
- navigation-routes.ts: refactored to named constants for readability
|
||||
- Structural quality gate: 3 warnings → 1 warning (false positive on factory function)
|
||||
|
||||
### Gauntlet Results
|
||||
- Phase 0 (Lint): PASSED
|
||||
- Phase 1 (Type-check): PASSED (0 errors)
|
||||
- Phase 2 (Frontend tests): PASSED (71/71)
|
||||
- Phase 3 (Rust tests): PASSED (7/7)
|
||||
- Phase 4 (Build): PASSED
|
||||
- Phase 5 (Structural quality): PASSED (8/8 gates, 1 warning)
|
||||
- Big Bird: Core ALL GREEN, Maestro ALL GREEN, Design ALL GREEN
|
||||
- Deadpool: All hard gates passed
|
||||
|
||||
## [0.7.4] — 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+1
-1
@@ -667,7 +667,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conductor-desktop"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "conductor-frontend",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.5",
|
||||
"dependencies": {
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"@tauri-apps/plugin-process": "^2.3.1"
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
settingsSections,
|
||||
defaultPlatforms,
|
||||
statusLabel,
|
||||
primaryActionLabel,
|
||||
type SettingsSectionId,
|
||||
type PlatformInfo,
|
||||
type PlatformConnectionStatus
|
||||
} from './settings-model';
|
||||
import AppBaseMetadata from './AppBaseMetadata.svelte';
|
||||
import UpdateChecker from '$lib/components/UpdateChecker.svelte';
|
||||
@@ -14,155 +11,29 @@
|
||||
import PlatformPrivacyScreen from '$lib/components/PlatformPrivacyScreen.svelte';
|
||||
import { concreteDate } from '$lib/utils/time';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import {
|
||||
getYoutubeStatus,
|
||||
startYoutubeConnect,
|
||||
disconnectYoutube,
|
||||
onYoutubeConnectionChanged,
|
||||
type YoutubeConnectionStatus
|
||||
} from './platform-bridge';
|
||||
import { createYoutubeConnection } from './youtube-connection.svelte';
|
||||
|
||||
let activeSectionId = $state<SettingsSectionId>('connected-platforms');
|
||||
|
||||
|
||||
let quietModeEnabled = $derived(settingsStore.quietMode);
|
||||
|
||||
let highContrastEnabled = $derived(settingsStore.highContrast);
|
||||
|
||||
let reduceMotionEnabled = $derived(settingsStore.reduceMotion);
|
||||
|
||||
let largeTextEnabled = $derived(settingsStore.largeText);
|
||||
|
||||
let dyslexiaEnabled = $derived(settingsStore.dyslexiaFriendly);
|
||||
let pendingPrivacyPlatform = $state<string | null>(null);
|
||||
|
||||
function setQuietMode(enabled: boolean) {
|
||||
settingsStore.setQuietMode(enabled);
|
||||
}
|
||||
const yt = createYoutubeConnection();
|
||||
let platforms = $derived(yt.platforms);
|
||||
let creatorPlatforms = $derived(yt.creatorPlatforms);
|
||||
let musicPlatforms = $derived(yt.musicPlatforms);
|
||||
let pendingPrivacyPlatform = $derived(yt.pendingPrivacyPlatform);
|
||||
|
||||
function setHighContrast(enabled: boolean) {
|
||||
settingsStore.setHighContrast(enabled);
|
||||
}
|
||||
function setReduceMotion(enabled: boolean) {
|
||||
settingsStore.setReduceMotion(enabled);
|
||||
}
|
||||
function setLargeText(enabled: boolean) {
|
||||
settingsStore.setLargeText(enabled);
|
||||
}
|
||||
function setDyslexiaFriendly(enabled: boolean) {
|
||||
settingsStore.setDyslexiaFriendly(enabled);
|
||||
}
|
||||
function setQuietMode(enabled: boolean) { settingsStore.setQuietMode(enabled); }
|
||||
function setHighContrast(enabled: boolean) { settingsStore.setHighContrast(enabled); }
|
||||
function setReduceMotion(enabled: boolean) { settingsStore.setReduceMotion(enabled); }
|
||||
function setLargeText(enabled: boolean) { settingsStore.setLargeText(enabled); }
|
||||
function setDyslexiaFriendly(enabled: boolean) { settingsStore.setDyslexiaFriendly(enabled); }
|
||||
|
||||
// Reactive platform state — starts from defaults, updates on user actions
|
||||
let platforms = $state<PlatformInfo[]>([...defaultPlatforms]);
|
||||
const creatorPlatforms = $derived(platforms.filter((p) => p.category === 'creator'));
|
||||
const musicPlatforms = $derived(platforms.filter((p) => p.category === 'music'));
|
||||
function updatePlatformStatus(id: string, status: PlatformConnectionStatus, extra?: Partial<PlatformInfo>) {
|
||||
platforms = platforms.map((p) =>
|
||||
p.id === id ? { ...p, status, ...extra } : p
|
||||
);
|
||||
}
|
||||
/** Map backend YoutubeConnectionStatus to frontend platform state. */
|
||||
function applyYoutubeStatus(status: YoutubeConnectionStatus) {
|
||||
if (status.connected) {
|
||||
updatePlatformStatus('youtube', 'connected', {
|
||||
accountName: status.accountName,
|
||||
lastSync: status.lastSync
|
||||
});
|
||||
} else if (status.needsReconnect) {
|
||||
updatePlatformStatus('youtube', 'needs-reconnect', {
|
||||
accountName: undefined,
|
||||
lastSync: undefined
|
||||
});
|
||||
} else {
|
||||
updatePlatformStatus('youtube', 'not-connected', {
|
||||
accountName: undefined,
|
||||
lastSync: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
/** Check YouTube connection status on mount. */
|
||||
$effect(() => {
|
||||
getYoutubeStatus().then(applyYoutubeStatus);
|
||||
});
|
||||
/** Listen for backend connection-changed events. */
|
||||
$effect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
onYoutubeConnectionChanged((status) => {
|
||||
applyYoutubeStatus(status);
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
return () => {
|
||||
unlisten?.();
|
||||
};
|
||||
});
|
||||
async function handlePlatformAction(platform: PlatformInfo) {
|
||||
if (platform.id === 'youtube') {
|
||||
await handleYoutubeAction(platform);
|
||||
}
|
||||
// Other platform connectors will be added later
|
||||
}
|
||||
async function handleYoutubeAction(platform: PlatformInfo) {
|
||||
switch (platform.status) {
|
||||
case 'not-configured': {
|
||||
// Tried connecting but credentials missing — show setup guide
|
||||
break;
|
||||
}
|
||||
case 'not-connected':
|
||||
case 'needs-reconnect':
|
||||
case 'error': {
|
||||
if (pendingPrivacyPlatform !== 'youtube') {
|
||||
pendingPrivacyPlatform = 'youtube';
|
||||
return;
|
||||
}
|
||||
pendingPrivacyPlatform = null;
|
||||
updatePlatformStatus('youtube', 'connecting');
|
||||
try {
|
||||
await startYoutubeConnect();
|
||||
// Poll for connection status with retries (avoids race condition)
|
||||
let retries = 0;
|
||||
const maxRetries = 10;
|
||||
while (retries < maxRetries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
try {
|
||||
const status = await getYoutubeStatus();
|
||||
applyYoutubeStatus(status);
|
||||
if (status.connected || status.needsReconnect || status.error) break;
|
||||
} catch {
|
||||
// Status fetch may fail during connection setup — retry
|
||||
}
|
||||
retries++;
|
||||
}
|
||||
} catch (err) {
|
||||
updatePlatformStatus('youtube', 'error', {
|
||||
errorMessage: err instanceof Error ? err.message : 'Could not connect to YouTube. Try again.'
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'connecting': {
|
||||
updatePlatformStatus('youtube', 'not-connected');
|
||||
break;
|
||||
}
|
||||
case 'connected': {
|
||||
const confirmed = window.confirm(
|
||||
`Disconnect YouTube? Conductor will stop posting and reading data from ${platform.accountName ?? 'your channel'}. Your data already in Conductor is kept.`
|
||||
);
|
||||
if (confirmed) {
|
||||
try {
|
||||
await disconnectYoutube();
|
||||
} finally {
|
||||
updatePlatformStatus('youtube', 'not-connected', {
|
||||
accountName: undefined,
|
||||
lastSync: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
$effect(() => { return yt.init(); });
|
||||
</script>
|
||||
<div class="settings-layout" aria-label="Settings">
|
||||
<div class="settings-header" aria-label="Settings location">
|
||||
@@ -199,7 +70,7 @@
|
||||
<PlatformPrivacyNotice
|
||||
platformName="YouTube"
|
||||
onCancel={() => { pendingPrivacyPlatform = null; }}
|
||||
onConfirm={() => handleYoutubeAction(platforms.find((p) => p.id === 'youtube')!)}
|
||||
onConfirm={() => yt.handlePlatformAction(platforms.find((p) => p.id === 'youtube')!)}
|
||||
/>
|
||||
{/if}
|
||||
<p class="settings-intro">Connect your social media accounts to make Conductor work for you.</p>
|
||||
@@ -249,7 +120,7 @@ export CONDUCTOR_YOUTUBE_CLIENT_SECRET="YOUR_SECRET"</pre>
|
||||
type="button"
|
||||
class="platform-action"
|
||||
aria-label="{primaryActionLabel(platform.status)} {platform.name}"
|
||||
onclick={() => handlePlatformAction(platform)}
|
||||
onclick={() => yt.handlePlatformAction(platform)}
|
||||
>
|
||||
{primaryActionLabel(platform.status)}
|
||||
</button>
|
||||
@@ -275,7 +146,7 @@ export CONDUCTOR_YOUTUBE_CLIENT_SECRET="YOUR_SECRET"</pre>
|
||||
type="button"
|
||||
class="platform-action"
|
||||
aria-label="{primaryActionLabel(platform.status)} {platform.name}"
|
||||
onclick={() => handlePlatformAction(platform)}
|
||||
onclick={() => yt.handlePlatformAction(platform)}
|
||||
>
|
||||
{primaryActionLabel(platform.status)}
|
||||
</button>
|
||||
@@ -406,11 +277,6 @@ export CONDUCTOR_YOUTUBE_CLIENT_SECRET="YOUR_SECRET"</pre>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.update-status{display:flex;align-items:center;gap:.75rem;margin:1rem 0}
|
||||
.status-indicator{width:10px;height:10px;border-radius:50%;background:var(--text-dim,#8b949e)}
|
||||
.status-indicator.connected{background:var(--success,#3fb950)}
|
||||
.action-button{background:var(--surface,#161b22);border:1px solid var(--border,#30363d);color:var(--text,#c9d1d9);padding:.5rem 1rem;border-radius:6px;cursor:pointer}
|
||||
.action-button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.toggle-row{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--border,#30363d)}
|
||||
.toggle-label{display:flex;flex-direction:column;gap:.15rem}
|
||||
.toggle-hint{font-size:.8rem;color:var(--text-dim,#8b949e)}
|
||||
|
||||
@@ -1,171 +1,180 @@
|
||||
import type { AppBaseCoachContext, AppBaseRoute } from './navigation';
|
||||
|
||||
|
||||
|
||||
|
||||
export const brandMark = 'C' as const;
|
||||
|
||||
function coachContext(heading: string, prompt: string, suggestions: readonly string[]): AppBaseCoachContext {
|
||||
return { heading, prompt, suggestions, triggerLabel: 'Coach', placement: 'top-right-route-action' as const, panel: 'compact-right-drawer' as const, closedSurface: 'none' as const, motion: 'right-to-left-drawer' as const };
|
||||
}
|
||||
|
||||
const dailyFeedRoute: AppBaseRoute = {
|
||||
id: 'daily-feed',
|
||||
label: 'Daily Feed',
|
||||
state: 'functional' as const,
|
||||
heading: 'Daily Feed',
|
||||
statusLabel: 'Ready',
|
||||
accessibleLabel: 'Daily Feed — what to make next, what needs attention, and low-energy quick wins',
|
||||
description: 'A focused landing spot showing today\'s next actions, scheduled posts, and quick wins.',
|
||||
brandMark,
|
||||
primaryActionLabel: 'Start something',
|
||||
belongsHere: ['Today\'s next action', 'Scheduled post reminders', 'Quick wins <5 min'],
|
||||
comingLater: ['Energy-aware sorting', 'Focus suggestions'],
|
||||
coachContext: coachContext('Daily Feed', 'Start with one clear action.', [
|
||||
'Pick a quick win',
|
||||
'Review today\'s schedule',
|
||||
'Plan tomorrow'
|
||||
]),
|
||||
};
|
||||
|
||||
const contentLibraryRoute: AppBaseRoute = {
|
||||
id: 'content-library',
|
||||
label: 'Content',
|
||||
state: 'functional' as const,
|
||||
heading: 'Content Library',
|
||||
statusLabel: 'Ready',
|
||||
accessibleLabel: 'Content library — manage your video files and uploads',
|
||||
description: 'Store, preview, and manage your video content before uploading.',
|
||||
brandMark: 'C',
|
||||
primaryActionLabel: 'Add content',
|
||||
coachContext: coachContext('Content Library', 'Manage video files, preview content, and prepare for upload.', [
|
||||
'Add new video files',
|
||||
'Preview your content before uploading',
|
||||
'Organise your media library',
|
||||
]),
|
||||
belongsHere: ['Video storage', 'Preview', 'Batch upload'],
|
||||
comingLater: ['AI content suggestions', 'Auto-tagging']
|
||||
};
|
||||
|
||||
const postsRoute: AppBaseRoute = {
|
||||
id: 'posts',
|
||||
label: 'Posts',
|
||||
state: 'planned-placeholder',
|
||||
heading: 'Posts',
|
||||
statusLabel: 'Publishing workspace planned',
|
||||
accessibleLabel: 'Posts view for drafts, scheduled posts, Scheduler, video upload, and thumbnails',
|
||||
description:
|
||||
'Posts owns the publishing workflow: create content, keep drafts, manage scheduled posts, upload video, prepare thumbnails, and recover missed posts.',
|
||||
primaryActionLabel: 'Create post',
|
||||
belongsHere: [
|
||||
'Create post',
|
||||
'Drafts',
|
||||
'Scheduled posts',
|
||||
'Scheduler',
|
||||
'Video upload',
|
||||
'Thumbnail selection and editing',
|
||||
'Templates',
|
||||
'Missed post recovery'
|
||||
],
|
||||
comingLater: [
|
||||
'Platform-specific previews',
|
||||
'Per-platform scheduling',
|
||||
'Media library attachment flow',
|
||||
'Thumbnail generation from video frames'
|
||||
],
|
||||
coachContext: coachContext('Posts', 'Improve drafting, scheduling, upload, and thumbnail workflow.', [
|
||||
'Ask how to reduce friction from idea to draft',
|
||||
'Tune thumbnail help so it stays inside video upload'
|
||||
]),
|
||||
brandMark
|
||||
};
|
||||
|
||||
const analyticsRoute: AppBaseRoute = {
|
||||
id: 'analytics',
|
||||
label: 'Analytics',
|
||||
state: 'planned-placeholder',
|
||||
heading: 'Analytics',
|
||||
statusLabel: 'Performance workspace planned',
|
||||
accessibleLabel: 'Analytics view for performance, trends, ads, revenue, and goals',
|
||||
description:
|
||||
'Analytics shows what worked and what to do more of: content performance, audience signals, trends, Ads, revenue, conversions, and goals.',
|
||||
primaryActionLabel: 'Review performance',
|
||||
belongsHere: [
|
||||
'Content performance',
|
||||
'Audience signals',
|
||||
'Trends',
|
||||
'Ads and campaign performance',
|
||||
'Revenue and conversions',
|
||||
'Goal progress'
|
||||
],
|
||||
comingLater: [
|
||||
'Platform analytics connectors',
|
||||
'Ad spend and ROI views',
|
||||
'Trend interpretation',
|
||||
'Goal-weighted recommendations'
|
||||
],
|
||||
coachContext: coachContext('Analytics', 'Improve how performance data becomes next actions.', [
|
||||
'Ask how ads should connect to content performance',
|
||||
'Tune goal-weighted recommendations'
|
||||
]),
|
||||
brandMark
|
||||
};
|
||||
|
||||
const settingsRoute: AppBaseRoute = {
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
state: 'functional',
|
||||
heading: 'Settings',
|
||||
statusLabel: 'Settings home ready',
|
||||
accessibleLabel: 'Settings view for preferences, connected platforms, updates, privacy, and About',
|
||||
description:
|
||||
'Settings is for configuration, not daily work: preferences, connected platforms, notifications, data and privacy, updates, and a simple About area.',
|
||||
primaryActionLabel: 'Review settings',
|
||||
belongsHere: [
|
||||
'Connected platforms and accounts',
|
||||
'Notifications',
|
||||
'Local relay and Pi setup',
|
||||
'Data and privacy',
|
||||
'Updates',
|
||||
'About'
|
||||
],
|
||||
comingLater: [
|
||||
'Account connection health',
|
||||
'Update checks and release notes',
|
||||
'Local relay setup wizard',
|
||||
'Data retention preferences'
|
||||
],
|
||||
coachContext: coachContext('Settings', 'Improve setup without exposing unnecessary technical noise.', [
|
||||
'Ask what settings are too technical',
|
||||
'Tune connected platform setup steps'
|
||||
]),
|
||||
brandMark
|
||||
};
|
||||
|
||||
const calendarRoute: AppBaseRoute = {
|
||||
id: 'calendar',
|
||||
label: 'Scheduled Posts',
|
||||
state: 'planned-placeholder',
|
||||
statusLabel: 'Coming Soon',
|
||||
heading: 'Calendar',
|
||||
accessibleLabel: 'Content calendar',
|
||||
description: 'Visual schedule of upcoming posts.',
|
||||
brandMark,
|
||||
primaryActionLabel: 'Coming Soon',
|
||||
coachContext: coachContext('Calendar', 'Plan your posting schedule.', ['View upcoming posts', 'Reschedule posts']),
|
||||
belongsHere: [],
|
||||
comingLater: [],
|
||||
};
|
||||
|
||||
const commentsRoute: AppBaseRoute = {
|
||||
id: 'comments',
|
||||
label: 'Comments',
|
||||
state: 'planned-placeholder',
|
||||
statusLabel: 'Coming Soon',
|
||||
heading: 'Comments',
|
||||
accessibleLabel: 'Comment inbox',
|
||||
description: 'Unified comment inbox across platforms.',
|
||||
brandMark,
|
||||
primaryActionLabel: 'Coming Soon',
|
||||
coachContext: coachContext('Comments', 'Manage comments.', ['View comments', 'Reply to comments']),
|
||||
belongsHere: [],
|
||||
comingLater: [],
|
||||
};
|
||||
|
||||
export const appBaseRoutes: readonly AppBaseRoute[] = [
|
||||
{
|
||||
id: 'daily-feed',
|
||||
label: 'Daily Feed',
|
||||
state: 'functional' as const,
|
||||
heading: 'Daily Feed',
|
||||
statusLabel: 'Ready',
|
||||
accessibleLabel: 'Daily Feed — what to make next, what needs attention, and low-energy quick wins',
|
||||
description: 'A focused landing spot showing today\'s next actions, scheduled posts, and quick wins.',
|
||||
brandMark,
|
||||
primaryActionLabel: 'Start something',
|
||||
belongsHere: ['Today\'s next action', 'Scheduled post reminders', 'Quick wins <5 min'],
|
||||
comingLater: ['Energy-aware sorting', 'Focus suggestions'],
|
||||
coachContext: coachContext('Daily Feed', 'Start with one clear action.', [
|
||||
'Pick a quick win',
|
||||
'Review today\'s schedule',
|
||||
'Plan tomorrow'
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 'content-library',
|
||||
label: 'Content',
|
||||
state: 'functional' as const,
|
||||
heading: 'Content Library',
|
||||
statusLabel: 'Ready',
|
||||
accessibleLabel: 'Content library — manage your video files and uploads',
|
||||
description: 'Store, preview, and manage your video content before uploading.',
|
||||
brandMark: 'C',
|
||||
primaryActionLabel: 'Add content',
|
||||
coachContext: coachContext('Content Library', 'Manage video files, preview content, and prepare for upload.', [
|
||||
'Add new video files',
|
||||
'Preview your content before uploading',
|
||||
'Organise your media library',
|
||||
]),
|
||||
belongsHere: ['Video storage', 'Preview', 'Batch upload'],
|
||||
comingLater: ['AI content suggestions', 'Auto-tagging']
|
||||
},
|
||||
{
|
||||
id: 'posts',
|
||||
label: 'Posts',
|
||||
state: 'planned-placeholder',
|
||||
heading: 'Posts',
|
||||
statusLabel: 'Publishing workspace planned',
|
||||
accessibleLabel: 'Posts view for drafts, scheduled posts, Scheduler, video upload, and thumbnails',
|
||||
description:
|
||||
'Posts owns the publishing workflow: create content, keep drafts, manage scheduled posts, upload video, prepare thumbnails, and recover missed posts.',
|
||||
primaryActionLabel: 'Create post',
|
||||
belongsHere: [
|
||||
'Create post',
|
||||
'Drafts',
|
||||
'Scheduled posts',
|
||||
'Scheduler',
|
||||
'Video upload',
|
||||
'Thumbnail selection and editing',
|
||||
'Templates',
|
||||
'Missed post recovery'
|
||||
],
|
||||
comingLater: [
|
||||
'Platform-specific previews',
|
||||
'Per-platform scheduling',
|
||||
'Media library attachment flow',
|
||||
'Thumbnail generation from video frames'
|
||||
],
|
||||
coachContext: coachContext('Posts', 'Improve drafting, scheduling, upload, and thumbnail workflow.', [
|
||||
'Ask how to reduce friction from idea to draft',
|
||||
'Tune thumbnail help so it stays inside video upload'
|
||||
]),
|
||||
brandMark
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
label: 'Analytics',
|
||||
state: 'planned-placeholder',
|
||||
heading: 'Analytics',
|
||||
statusLabel: 'Performance workspace planned',
|
||||
accessibleLabel: 'Analytics view for performance, trends, ads, revenue, and goals',
|
||||
description:
|
||||
'Analytics shows what worked and what to do more of: content performance, audience signals, trends, Ads, revenue, conversions, and goals.',
|
||||
primaryActionLabel: 'Review performance',
|
||||
belongsHere: [
|
||||
'Content performance',
|
||||
'Audience signals',
|
||||
'Trends',
|
||||
'Ads and campaign performance',
|
||||
'Revenue and conversions',
|
||||
'Goal progress'
|
||||
],
|
||||
comingLater: [
|
||||
'Platform analytics connectors',
|
||||
'Ad spend and ROI views',
|
||||
'Trend interpretation',
|
||||
'Goal-weighted recommendations'
|
||||
],
|
||||
coachContext: coachContext('Analytics', 'Improve how performance data becomes next actions.', [
|
||||
'Ask how ads should connect to content performance',
|
||||
'Tune goal-weighted recommendations'
|
||||
]),
|
||||
brandMark
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
state: 'functional',
|
||||
heading: 'Settings',
|
||||
statusLabel: 'Settings home ready',
|
||||
accessibleLabel: 'Settings view for preferences, connected platforms, updates, privacy, and About',
|
||||
description:
|
||||
'Settings is for configuration, not daily work: preferences, connected platforms, notifications, data and privacy, updates, and a simple About area.',
|
||||
primaryActionLabel: 'Review settings',
|
||||
belongsHere: [
|
||||
'Connected platforms and accounts',
|
||||
'Notifications',
|
||||
'Local relay and Pi setup',
|
||||
'Data and privacy',
|
||||
'Updates',
|
||||
'About'
|
||||
],
|
||||
comingLater: [
|
||||
'Account connection health',
|
||||
'Update checks and release notes',
|
||||
'Local relay setup wizard',
|
||||
'Data retention preferences'
|
||||
],
|
||||
coachContext: coachContext('Settings', 'Improve setup without exposing unnecessary technical noise.', [
|
||||
'Ask what settings are too technical',
|
||||
'Tune connected platform setup steps'
|
||||
]),
|
||||
brandMark
|
||||
},
|
||||
{
|
||||
id: 'calendar',
|
||||
label: 'Scheduled Posts',
|
||||
state: 'planned-placeholder',
|
||||
statusLabel: 'Coming Soon',
|
||||
heading: 'Calendar',
|
||||
accessibleLabel: 'Content calendar',
|
||||
description: 'Visual schedule of upcoming posts.',
|
||||
brandMark,
|
||||
primaryActionLabel: 'Coming Soon',
|
||||
coachContext: coachContext('Calendar', 'Plan your posting schedule.', ['View upcoming posts', 'Reschedule posts']),
|
||||
belongsHere: [],
|
||||
comingLater: [],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'comments',
|
||||
label: 'Comments',
|
||||
state: 'planned-placeholder',
|
||||
statusLabel: 'Coming Soon',
|
||||
heading: 'Comments',
|
||||
accessibleLabel: 'Comment inbox',
|
||||
description: 'Unified comment inbox across platforms.',
|
||||
brandMark,
|
||||
primaryActionLabel: 'Coming Soon',
|
||||
coachContext: coachContext('Comments', 'Manage comments.', ['View comments', 'Reply to comments']),
|
||||
belongsHere: [],
|
||||
comingLater: [],
|
||||
},
|
||||
];
|
||||
dailyFeedRoute,
|
||||
contentLibraryRoute,
|
||||
postsRoute,
|
||||
analyticsRoute,
|
||||
settingsRoute,
|
||||
calendarRoute,
|
||||
commentsRoute,
|
||||
];
|
||||
@@ -0,0 +1,133 @@
|
||||
import { defaultPlatforms, type PlatformInfo, type PlatformConnectionStatus } from './settings-model';
|
||||
import {
|
||||
getYoutubeStatus,
|
||||
startYoutubeConnect,
|
||||
disconnectYoutube,
|
||||
onYoutubeConnectionChanged,
|
||||
type YoutubeConnectionStatus
|
||||
} from './platform-bridge';
|
||||
|
||||
export function createYoutubeConnection() {
|
||||
let platforms = $state<PlatformInfo[]>([...defaultPlatforms]);
|
||||
let pendingPrivacyPlatform = $state<string | null>(null);
|
||||
|
||||
const creatorPlatforms = $derived(platforms.filter((p) => p.category === 'creator'));
|
||||
const musicPlatforms = $derived(platforms.filter((p) => p.category === 'music'));
|
||||
|
||||
function updatePlatformStatus(id: string, status: PlatformConnectionStatus, extra?: Partial<PlatformInfo>) {
|
||||
platforms = platforms.map((p) =>
|
||||
p.id === id ? { ...p, status, ...extra } : p
|
||||
);
|
||||
}
|
||||
|
||||
function applyYoutubeStatus(status: YoutubeConnectionStatus) {
|
||||
if (status.connected) {
|
||||
updatePlatformStatus('youtube', 'connected', {
|
||||
accountName: status.accountName,
|
||||
lastSync: status.lastSync
|
||||
});
|
||||
} else if (status.needsReconnect) {
|
||||
updatePlatformStatus('youtube', 'needs-reconnect', {
|
||||
accountName: undefined,
|
||||
lastSync: undefined
|
||||
});
|
||||
} else {
|
||||
updatePlatformStatus('youtube', 'not-connected', {
|
||||
accountName: undefined,
|
||||
lastSync: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
getYoutubeStatus().then(applyYoutubeStatus);
|
||||
|
||||
let unlisten: (() => void) | undefined;
|
||||
onYoutubeConnectionChanged((status) => {
|
||||
applyYoutubeStatus(status);
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten?.();
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePlatformAction(platform: PlatformInfo) {
|
||||
if (platform.id === 'youtube') {
|
||||
await handleYoutubeAction(platform);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleYoutubeAction(platform: PlatformInfo) {
|
||||
switch (platform.status) {
|
||||
case 'not-configured': {
|
||||
break;
|
||||
}
|
||||
case 'not-connected':
|
||||
case 'needs-reconnect':
|
||||
case 'error': {
|
||||
if (pendingPrivacyPlatform !== 'youtube') {
|
||||
pendingPrivacyPlatform = 'youtube';
|
||||
return;
|
||||
}
|
||||
pendingPrivacyPlatform = null;
|
||||
updatePlatformStatus('youtube', 'connecting');
|
||||
try {
|
||||
await startYoutubeConnect();
|
||||
let retries = 0;
|
||||
const maxRetries = 10;
|
||||
while (retries < maxRetries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
try {
|
||||
const status = await getYoutubeStatus();
|
||||
applyYoutubeStatus(status);
|
||||
if (status.connected || status.needsReconnect || status.error) break;
|
||||
} catch {
|
||||
// Status fetch may fail during connection setup — retry
|
||||
}
|
||||
retries++;
|
||||
}
|
||||
} catch (err) {
|
||||
updatePlatformStatus('youtube', 'error', {
|
||||
errorMessage: err instanceof Error ? err.message : 'Could not connect to YouTube. Try again.'
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'connecting': {
|
||||
updatePlatformStatus('youtube', 'not-connected');
|
||||
break;
|
||||
}
|
||||
case 'connected': {
|
||||
const confirmed = window.confirm(
|
||||
`Disconnect YouTube? Conductor will stop posting and reading data from ${platform.accountName ?? 'your channel'}. Your data already in Conductor is kept.`
|
||||
);
|
||||
if (confirmed) {
|
||||
try {
|
||||
await disconnectYoutube();
|
||||
} finally {
|
||||
updatePlatformStatus('youtube', 'not-connected', {
|
||||
accountName: undefined,
|
||||
lastSync: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get platforms() { return platforms; },
|
||||
get creatorPlatforms() { return creatorPlatforms; },
|
||||
get musicPlatforms() { return musicPlatforms; },
|
||||
get pendingPrivacyPlatform() { return pendingPrivacyPlatform; },
|
||||
set pendingPrivacyPlatform(value: string | null) { pendingPrivacyPlatform = value; },
|
||||
init,
|
||||
handlePlatformAction,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import UpdateConfirmDialog from './UpdateConfirmDialog.svelte';
|
||||
|
||||
type UpdateState = 'idle' | 'checking' | 'up-to-date' | 'available' | 'pending' | 'installing' | 'installed' | 'error';
|
||||
|
||||
@@ -176,24 +177,12 @@
|
||||
</div>
|
||||
|
||||
{#if showConfirmDialog}
|
||||
<div class="confirm-overlay" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
||||
<div class="confirm-dialog">
|
||||
<h3 id="confirm-title">Update Conductor?</h3>
|
||||
<p>
|
||||
Version <strong>{remoteVersion}</strong> is available.
|
||||
You are currently on <strong>{currentVersion}</strong>.
|
||||
</p>
|
||||
<p class="confirm-hint">The app will restart after the update is installed.</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="confirm-yes" onclick={confirmUpdate} aria-label="Confirm update">
|
||||
Yes, update
|
||||
</button>
|
||||
<button class="confirm-no" onclick={declineUpdate} aria-label="Decline update">
|
||||
Not now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UpdateConfirmDialog
|
||||
currentVersion={currentVersion}
|
||||
remoteVersion={remoteVersion}
|
||||
onConfirm={confirmUpdate}
|
||||
onDecline={declineUpdate}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -270,68 +259,4 @@
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.confirm-dialog {
|
||||
background: var(--color-surface-overlay, #161b22);
|
||||
border: 1px solid var(--color-border-default, #30363d);
|
||||
border-radius: 0.75rem;
|
||||
padding: var(--space-xl, 1.5rem);
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 40%);
|
||||
}
|
||||
.confirm-dialog h3 {
|
||||
margin: 0 0 var(--space-md, 0.75rem);
|
||||
color: var(--color-text-primary, #ffffff);
|
||||
font-size: var(--font-size-lg, 1.1rem);
|
||||
}
|
||||
.confirm-dialog p {
|
||||
margin: 0.3rem 0;
|
||||
color: var(--color-text-secondary, #9fb0ca);
|
||||
font-size: var(--font-size-base, 0.9rem);
|
||||
}
|
||||
.confirm-hint {
|
||||
font-size: var(--font-size-sm, 0.8rem) !important;
|
||||
color: var(--color-text-dim, #8b949e);
|
||||
}
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: var(--space-md, 0.75rem);
|
||||
margin-top: var(--space-lg, 1.25rem);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.confirm-yes, .confirm-no {
|
||||
padding: var(--space-sm, 0.5rem) var(--space-lg, 1.25rem);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-border-default, rgb(255 255 255 / 15%));
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base, 0.9rem);
|
||||
}
|
||||
.confirm-yes {
|
||||
background: color-mix(in oklch, var(--color-success, oklch(0.62 0.18 145)) 20%, transparent);
|
||||
border-color: color-mix(in oklch, var(--color-success, oklch(0.62 0.18 145)) 40%, transparent);
|
||||
color: var(--color-success, #7ee99b);
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirm-yes:hover {
|
||||
background: color-mix(in oklch, var(--color-success, oklch(0.62 0.18 145)) 30%, transparent);
|
||||
}
|
||||
.confirm-no {
|
||||
background: var(--color-surface-raised, rgb(255 255 255 / 5%));
|
||||
color: var(--color-text-secondary, #9fb0ca);
|
||||
}
|
||||
.confirm-no:hover {
|
||||
background: var(--color-surface-overlay, rgb(255 255 255 / 10%));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
let { currentVersion, remoteVersion, onConfirm, onDecline }: {
|
||||
currentVersion: string;
|
||||
remoteVersion: string;
|
||||
onConfirm: () => void;
|
||||
onDecline: () => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="confirm-overlay" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
||||
<div class="confirm-dialog">
|
||||
<h3 id="confirm-title">Update Conductor?</h3>
|
||||
<p>
|
||||
Version <strong>{remoteVersion}</strong> is available.
|
||||
You are currently on <strong>{currentVersion}</strong>.
|
||||
</p>
|
||||
<p class="confirm-hint">The app will restart after the update is installed.</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="confirm-yes" onclick={onConfirm} aria-label="Confirm update">
|
||||
Yes, update
|
||||
</button>
|
||||
<button class="confirm-no" onclick={onDecline} aria-label="Decline update">
|
||||
Not now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.confirm-dialog {
|
||||
background: var(--color-surface-overlay, #161b22);
|
||||
border: 1px solid var(--color-border-default, #30363d);
|
||||
border-radius: 0.75rem;
|
||||
padding: var(--space-xl, 1.5rem);
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 40%);
|
||||
}
|
||||
.confirm-dialog h3 {
|
||||
margin: 0 0 var(--space-md, 0.75rem);
|
||||
color: var(--color-text-primary, #ffffff);
|
||||
font-size: var(--font-size-lg, 1.1rem);
|
||||
}
|
||||
.confirm-dialog p {
|
||||
margin: 0.3rem 0;
|
||||
color: var(--color-text-secondary, #9fb0ca);
|
||||
font-size: var(--font-size-base, 0.9rem);
|
||||
}
|
||||
.confirm-hint {
|
||||
font-size: var(--font-size-sm, 0.8rem) !important;
|
||||
color: var(--color-text-dim, #8b949e);
|
||||
}
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: var(--space-md, 0.75rem);
|
||||
margin-top: var(--space-lg, 1.25rem);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.confirm-yes, .confirm-no {
|
||||
padding: var(--space-sm, 0.5rem) var(--space-lg, 1.25rem);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-border-default, rgb(255 255 255 / 15%));
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base, 0.9rem);
|
||||
}
|
||||
.confirm-yes {
|
||||
background: color-mix(in oklch, var(--color-success, oklch(0.62 0.18 145)) 20%, transparent);
|
||||
border-color: color-mix(in oklch, var(--color-success, oklch(0.62 0.18 145)) 40%, transparent);
|
||||
color: var(--color-success, #7ee99b);
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirm-yes:hover {
|
||||
background: color-mix(in oklch, var(--color-success, oklch(0.62 0.18 145)) 30%, transparent);
|
||||
}
|
||||
.confirm-no {
|
||||
background: var(--color-surface-raised, rgb(255 255 255 / 5%));
|
||||
color: var(--color-text-secondary, #9fb0ca);
|
||||
}
|
||||
.confirm-no:hover {
|
||||
background: var(--color-surface-overlay, rgb(255 255 255 / 10%));
|
||||
}
|
||||
</style>
|
||||
@@ -59,26 +59,14 @@
|
||||
animation: tooltip-in 0.15s ease forwards;
|
||||
}
|
||||
|
||||
.tooltip.exiting {
|
||||
animation: tooltip-out 0.1s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes tooltip-in {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes tooltip-out {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tooltip {
|
||||
animation: none;
|
||||
}
|
||||
.tooltip.exiting {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,12 +14,4 @@
|
||||
<style>
|
||||
.comments-view { padding: 1rem 0; }
|
||||
.comments-placeholder { color: var(--color-text-dim); margin-bottom: 1.5rem; }
|
||||
.comment-preview-item {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid rgb(255 255 255 / 8%);
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.comment-platform { display: block; font-size: 0.75rem; color: var(--color-accent); margin-bottom: 0.35rem; text-transform: uppercase; }
|
||||
.comment-preview-item p { margin: 0; font-size: 0.9rem; }
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "conductor-desktop-app",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.4.0",
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ tag_format: "${component}@${version}"
|
||||
# Root version = project completeness toward 1.0.0
|
||||
# Root → 1.0.0 only when ALL components are production-ready and system works end-to-end
|
||||
root:
|
||||
version: 0.7.4
|
||||
version: 0.7.5
|
||||
bump_rules:
|
||||
component_patch: root_patch # 0.0.x
|
||||
component_minor: root_patch # 0.0.x typically, OR 0.x.0 for milestones
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "conductor-desktop"
|
||||
version = "0.7.4"
|
||||
version = "0.7.5"
|
||||
edition = "2024"
|
||||
description = "Conductor Tauri desktop shell"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Conductor",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.5",
|
||||
"identifier": "com.guitarguynick.conductor",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
Reference in New Issue
Block a user