Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/ModelPublisher.Core/Platforms/PrintablesPublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class PrintablesPublisher : IPlatformPublisher
public bool IsFreeOnly => false;
public bool SupportsMarkdown => false;

public string Disclaimer => "";
public string Disclaimer => GetDisclaimer();

public async Task<PublishResult> PublishFreeAsync(ReleaseManifest manifest, IPage page,
CancellationToken ct = default)
Expand Down Expand Up @@ -115,18 +115,13 @@ await page.
GetByRole(AriaRole.Button, new() { Name = "Creative Commons — Attribution — Noncommercial — Share Alike" })
.ClickAsync();

// Step 11: Human confirmation before publish
AnsiConsole.MarkupLine(
$"[yellow][[{PlatformName}]][/] Review the form in the browser. Press [green]Enter[/] to publish...");

await Task.Run(Console.ReadLine, ct);

// Step 12: Submit
// Step 11: Submit
await page
.GetByRole(AriaRole.Button, new() { Name = "Save draft" })
.ClickAsync();

await page.WaitForURLAsync("**/model/**", new() { Timeout = 30_000 });
// Wait for URL to change away from /model/create to the saved model URL (e.g. /model/12345-slug)
await page.WaitForURLAsync(url => url.Contains("/model/") && !url.Contains("/model/create"), new() { Timeout = 30_000 });

return new PublishResult(PlatformName, true, page.Url, null);
}
Expand All @@ -140,4 +135,11 @@ public Task<PublishResult> PublishPremiumAsync(ReleaseManifest manifest, IPage p
{
throw new NotImplementedException();
}

private static string GetDisclaimer()
{
return "_ALL The Crafty Maker designs are protected by Copyright Law. By downloading, YOU HAVE NO RIGHT to " +
"sell any digital files or reproductions from those files. If you want a commercial license to LEGALLY " +
"SELL 3D prints, you'll need a The Crafty Seller Patreon subscription._";
}
}
91 changes: 61 additions & 30 deletions src/ModelPublisher.Core/Platforms/ThangsPublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,23 @@ await page
.GetByTestId("action-upload-models")
.ClickAsync();

// Step 1: Model files
AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading model file...");

var modelFileInput = page.Locator("input[multiple]").First;
await modelFileInput.FocusAsync();

await FileUploadHelper.UploadSequentialAsync(
page, modelFileInput, manifest.Files.Models.Select(manifest.ResolveFilePath), PlatformName);

await page.GetByTestId("upload-mode-collection").ClickAsync();

await page.Locator("label[for='terms-acceptance']").ClickAsync();

// Step 1: Model files — intercept file chooser so React registers the selection
AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading model files...");

var modelChooser = await page
.RunAndWaitForFileChooserAsync(() => page.GetByTestId("full-page-upload-container").ClickAsync());

await modelChooser
.SetFilesAsync(manifest.Files.Models.Select(manifest.ResolveFilePath).ToArray());

var uploadMode = page.GetByTestId("upload-mode-collection");
await uploadMode.WaitForAsync();
await uploadMode.ClickAsync();

var terms = page.Locator("label[for='terms-acceptance']");
await terms.WaitForAsync();
await terms.ClickAsync();

await page
.GetByTestId("file-selector-buttons-upload-files")
.ClickAsync();
Expand Down Expand Up @@ -89,31 +93,58 @@ await page
await page.WaitForTimeoutAsync(300);
}

// Step 6: Upload photos
// Step 6: Upload photos — set all at once
AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading photos...");

var photoInput = page.Locator("input[multiple][accept*='.jpg']").First;
await FileUploadHelper.UploadSequentialAsync(
page, photoInput, manifest.Files.PhotosOrdered(coverFirst: false).Select(manifest.ResolveFilePath),
PlatformName);
await FileUploadHelper
.UploadToInputAsync(photoInput, manifest.Files.PhotosOrdered(coverFirst: false).Select(manifest.ResolveFilePath).ToArray());
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

// Step 7: Audience
await page.Locator("[class*='Audience_VisibilityItem']").First.ClickAsync();
await page.GetByText("Public sharing").ClickAsync();
// Step 7: Audience — retry clicking the trigger until the dialog opens
await page
.GetByTestId("model-upload-audience")
.WaitForAsync();

var audienceTrigger = page.Locator("span[aria-haspopup='dialog']:has([data-testid='model-upload-audience'])");
await audienceTrigger.ScrollIntoViewIfNeededAsync();
var publicSharingOption = page.GetByText("Public sharing");
for (var attempt = 0; attempt < 5; attempt++)
{
await audienceTrigger.ClickAsync();
try
{
await publicSharingOption.WaitForAsync(new() { Timeout = 2_000 });
break;
}
catch { /* dropdown didn't open, retry */ }
}
await publicSharingOption.ClickAsync();

// Step 7: License
await page.GetByRole(AriaRole.Button, new() { Name = "Select a license" }).ClickAsync();
await page.GetByText("by-nc-sa_4_0.txt").First.ClickAsync();

// Step 8: Human review before publishing
AnsiConsole.MarkupLine(
$"[yellow][[{PlatformName}]][/] Review the form in the browser. Press [green]Enter[/] to publish...");

await Task.Run(Console.ReadLine, ct);
await page
.GetByRole(AriaRole.Button, new() { Name = "Select a license" })
.ClickAsync();

await page
.GetByText("by-nc-sa_4_0.txt").First
.ClickAsync();

// Step 9: Save
await page.GetByTestId("save-model-details").ClickAsync();
// Step 8: Save — wait for button to be enabled, then click (Thangs uses data-test-id, not data-testid)
AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Saving...");
await Assertions
.Expect(page.Locator("[data-test-id='save-and-publish-model-details']"))
.ToBeEnabledAsync(new() { Timeout = 60_000 });

await page
.Locator("[data-test-id='save-and-publish-model-details']")
.ClickAsync();

AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Save clicked.");

// Step 9: Close the upload dialog (non-fatal if already closed/navigated)
await page.Locator("[class*='FullPageUpload_Header_CloseIcon']").ClickAsync();

await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

return new PublishResult(PlatformName, true, page.Url, null);
Expand Down
38 changes: 21 additions & 17 deletions src/ModelPublisher.Core/PublishCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,35 @@ public async Task<int> ExecuteAsync(
AnsiConsole.WriteLine();

var session = new PublishSession { ManifestPath = manifestPath };
var consoleLock = new object();

using var playwright = await Playwright.CreateAsync();

foreach (var (publisher, tier) in runnablePublishers)
var tasks = runnablePublishers.Select(async x =>
{
AnsiConsole.Write(new Rule($"[bold]{publisher.PlatformName}[/] [dim]({tier})[/]").LeftJustified());

await using var context = await BrowserContextFactory.GetPersistentContextAsync(
playwright, publisher.PlatformKey);
playwright, x.Publisher.PlatformKey);

var page = await context.NewPageAsync();

var result = tier == "premium"
? await publisher.PublishPremiumAsync(manifest, page, ct)
: await publisher.PublishFreeAsync(manifest, page, ct);

session.Results.Add(result with { Tier = tier! });

if (result.Success)
AnsiConsole.MarkupLine($"[green]✓ {result.Platform}[/] → {Markup.Escape(result.PublishedUrl ?? "")}");
else
AnsiConsole.MarkupLine($"[red]✗ {result.Platform}[/] — {Markup.Escape(result.ErrorMessage ?? "")}");

AnsiConsole.WriteLine();
}
var result = x.Tier == "premium"
? await x.Publisher.PublishPremiumAsync(manifest, page, ct)
: await x.Publisher.PublishFreeAsync(manifest, page, ct);

var resultWithTier = result with { Tier = x.Tier! };

lock (consoleLock)
{
if (result.Success)
AnsiConsole.MarkupLine($"[green]✓ {result.Platform}[/] → {Markup.Escape(result.PublishedUrl ?? "")}");
else
AnsiConsole.MarkupLine($"[red]✗ {result.Platform}[/] — {Markup.Escape(result.ErrorMessage ?? "")}");
}

return resultWithTier;
});

session.Results.AddRange(await Task.WhenAll(tasks));

// Summary
AnsiConsole.Write(new Rule("[bold]Summary[/]").LeftJustified());
Expand Down
Loading