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:
Nick
2026-06-23 20:54:58 +01:00
parent 2db3faff69
commit a06958c611
14 changed files with 450 additions and 420 deletions
+24
View File
@@ -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
View File
@@ -667,7 +667,7 @@ dependencies = [
[[package]]
name = "conductor-desktop"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"base64 0.22.1",
"chrono",
+1 -1
View File
@@ -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"
+17 -151
View File
@@ -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)}
+170 -161
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",