Skip to content

Commit 48fc6de

Browse files
trevhollidayclaude
andcommitted
Rip-to-worker queue, ripping screen redesign, HA notifications
Core architecture: - Move rip execution from UI process to background worker via rip_queue.json - RipWorker actor polls rip_queue.json, runs RipUseCase, hands off to encode queue - UI polls rip_queue.json + status.json for live progress; resets to idle if queue cleared - Sign worker binary with Apple Development cert to avoid TCC re-prompts Ripping screen: - Peeking carousel (35% wide cards, aspectFit, 15s interval) in top 45% of screen - Fetch all TMDB backdrops via /images endpoint (up to 12, sorted by vote average) - Media details below carousel: tagline, overview, crew grid (Director/Writers) - Left panel during rip: title, year, rating, runtime, genres, release date, progress - TMDB search results show poster thumbnail as leading element Title selection: - MAIN badge only on largest title (matches CLI behavior) - Audio format matches CLI: "English (TrueHD 7.1)" - Angle info shown when reported by MakeMKV (attr 42) - Non-main titles dimmed to 70% opacity Notifications: - Python-based HA webhook hook (avoids shell quoting issues with curl) - Fires on rip_complete, rip_failed, encode_complete, encode_failed UI polish: - Window opens automatically on launch (MenuBarIcon.onAppear) - Version shown in menu bar footer (v0.1.0 build number) - Logs view reads worker.log, auto-refreshes every 3s - Encode queue and history reversed (newest first) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1b20f8b commit 48fc6de

24 files changed

+1256
-405
lines changed

FrostscribeUI/FrostscribeUI.xcodeproj/project.pbxproj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
E1B2C3D4E5F6A7B8C9D0E1F7 /* QueueSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B2C3D4E5F6A7B8C9D0E1F3 /* QueueSectionView.swift */; };
1919
E1B2C3D4E5F6A7B8C9D0E1F8 /* QueueRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B2C3D4E5F6A7B8C9D0E1F4 /* QueueRowView.swift */; };
2020
E1B2C3D4E5F6A7B8C9D0E1F9 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B2C3D4E5F6A7B8C9D0E1F5 /* SettingsView.swift */; };
21+
LV01C3D4E5F6A7B8C9D0E1F2 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV01C3D4E5F6A7B8C9D0E1F1 /* LogsView.swift */; };
2122
F1B2C3D4E5F6A7B8C9D0E1F3 /* FrostscribeCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1B2C3D4E5F6A7B8C9D0E1F5 /* FrostscribeCore */; };
2223
AW01C3D4E5F6A7B8C9D0E1F2 /* AppSupportWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AW01C3D4E5F6A7B8C9D0E1F1 /* AppSupportWatcher.swift */; };
2324
F3B2C3D4E5F6A7B8C9D0E1F3 /* VigilWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2C3D4E5F6A7B8C9D0E1F1 /* VigilWatcher.swift */; };
@@ -37,6 +38,7 @@
3738
FA10C3D4E5F6A7B8C9D0E1F2 /* ConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA10C3D4E5F6A7B8C9D0E1F1 /* ConfirmationView.swift */; };
3839
FA11C3D4E5F6A7B8C9D0E1F2 /* RippingProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA11C3D4E5F6A7B8C9D0E1F1 /* RippingProgressView.swift */; };
3940
FA12C3D4E5F6A7B8C9D0E1F2 /* RipCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA12C3D4E5F6A7B8C9D0E1F1 /* RipCompleteView.swift */; };
41+
FA13C3D4E5F6A7B8C9D0E1F2 /* RipCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA13C3D4E5F6A7B8C9D0E1F3 /* RipCarouselView.swift */; };
4042
/* End PBXBuildFile section */
4143

4244
/* Begin PBXFileReference section */
@@ -58,6 +60,7 @@
5860
FA10C3D4E5F6A7B8C9D0E1F1 /* ConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationView.swift; sourceTree = "<group>"; };
5961
FA11C3D4E5F6A7B8C9D0E1F1 /* RippingProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RippingProgressView.swift; sourceTree = "<group>"; };
6062
FA12C3D4E5F6A7B8C9D0E1F1 /* RipCompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RipCompleteView.swift; sourceTree = "<group>"; };
63+
FA13C3D4E5F6A7B8C9D0E1F3 /* RipCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RipCarouselView.swift; sourceTree = "<group>"; };
6164
H3B2C3D4E5F6A7B8C9D0E1F1 /* DiskArbitration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DiskArbitration.framework; path = System/Library/Frameworks/DiskArbitration.framework; sourceTree = SDKROOT; };
6265
C1B2C3D4E5F6A7B8C9D0E1F5 /* FrostTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrostTheme.swift; sourceTree = "<group>"; };
6366
C1B2C3D4E5F6A7B8C9D0E1F6 /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = "<group>"; };
@@ -68,6 +71,7 @@
6871
D1B2C3D4E5F6A7B8C9D0E1F3 /* QueueSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueSectionView.swift; sourceTree = "<group>"; };
6972
D1B2C3D4E5F6A7B8C9D0E1F4 /* QueueRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueRowView.swift; sourceTree = "<group>"; };
7073
D1B2C3D4E5F6A7B8C9D0E1F5 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
74+
LV01C3D4E5F6A7B8C9D0E1F1 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = "<group>"; };
7175
FB01C3D4E5F6A7B8C9D0E1F1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
7276
D1B2C3D4E5F6A7B8C9D0E1F6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7377
D1B2C3D4E5F6A7B8C9D0E1F7 /* FrostscribeUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FrostscribeUI.entitlements; sourceTree = "<group>"; };
@@ -182,6 +186,7 @@
182186
D1B2C3D4E5F6A7B8C9D0E1F3 /* QueueSectionView.swift */,
183187
D1B2C3D4E5F6A7B8C9D0E1F4 /* QueueRowView.swift */,
184188
D1B2C3D4E5F6A7B8C9D0E1F5 /* SettingsView.swift */,
189+
LV01C3D4E5F6A7B8C9D0E1F1 /* LogsView.swift */,
185190
FA00C3D4E5F6A7B8C9D0E1F1 /* Rip */,
186191
);
187192
path = Views;
@@ -202,6 +207,7 @@
202207
FA10C3D4E5F6A7B8C9D0E1F1 /* ConfirmationView.swift */,
203208
FA11C3D4E5F6A7B8C9D0E1F1 /* RippingProgressView.swift */,
204209
FA12C3D4E5F6A7B8C9D0E1F1 /* RipCompleteView.swift */,
210+
FA13C3D4E5F6A7B8C9D0E1F3 /* RipCarouselView.swift */,
205211
);
206212
path = Rip;
207213
sourceTree = "<group>";
@@ -298,6 +304,7 @@
298304
E1B2C3D4E5F6A7B8C9D0E1F7 /* QueueSectionView.swift in Sources */,
299305
E1B2C3D4E5F6A7B8C9D0E1F8 /* QueueRowView.swift in Sources */,
300306
E1B2C3D4E5F6A7B8C9D0E1F9 /* SettingsView.swift in Sources */,
307+
LV01C3D4E5F6A7B8C9D0E1F2 /* LogsView.swift in Sources */,
301308
AW01C3D4E5F6A7B8C9D0E1F2 /* AppSupportWatcher.swift in Sources */,
302309
F3B2C3D4E5F6A7B8C9D0E1F3 /* VigilWatcher.swift in Sources */,
303310
F3B2C3D4E5F6A7B8C9D0E1F4 /* VigilViewModel.swift in Sources */,
@@ -315,6 +322,7 @@
315322
FA10C3D4E5F6A7B8C9D0E1F2 /* ConfirmationView.swift in Sources */,
316323
FA11C3D4E5F6A7B8C9D0E1F2 /* RippingProgressView.swift in Sources */,
317324
FA12C3D4E5F6A7B8C9D0E1F2 /* RipCompleteView.swift in Sources */,
325+
FA13C3D4E5F6A7B8C9D0E1F2 /* RipCarouselView.swift in Sources */,
318326
);
319327
runOnlyForDeploymentPostprocessing = 0;
320328
};
@@ -446,7 +454,7 @@
446454
CODE_SIGN_ENTITLEMENTS = FrostscribeUI/FrostscribeUI.entitlements;
447455
CODE_SIGN_STYLE = Automatic;
448456
COMBINE_HIDPI_IMAGES = YES;
449-
CURRENT_PROJECT_VERSION = 1;
457+
CURRENT_PROJECT_VERSION = 2;
450458
DEVELOPMENT_ASSET_PATHS = "";
451459
DEVELOPMENT_TEAM = "";
452460
ENABLE_HARDENED_RUNTIME = YES;
@@ -474,7 +482,7 @@
474482
CODE_SIGN_ENTITLEMENTS = FrostscribeUI/FrostscribeUI.entitlements;
475483
CODE_SIGN_STYLE = Automatic;
476484
COMBINE_HIDPI_IMAGES = YES;
477-
CURRENT_PROJECT_VERSION = 1;
485+
CURRENT_PROJECT_VERSION = 2;
478486
DEVELOPMENT_ASSET_PATHS = "";
479487
DEVELOPMENT_TEAM = "";
480488
ENABLE_HARDENED_RUNTIME = YES;

FrostscribeUI/FrostscribeUI/Design/FrostTheme.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ enum FrostTheme {
99
static let deepBlue = Color(.sRGB, red: 0.05, green: 0.15, blue: 0.3, opacity: 1)
1010
static let alert = Color(.sRGB, red: 1, green: 0.3, blue: 0.3, opacity: 1)
1111

12+
// UI chrome
13+
static let background = Color(.sRGB, red: 0.04, green: 0.22, blue: 0.28, opacity: 1)
14+
static let textPrimary = Color(.sRGB, red: 0.82, green: 0.90, blue: 0.93, opacity: 1)
15+
static let divider = Color(.sRGB, red: 0.1, green: 0.32, blue: 0.38, opacity: 1)
16+
1217
// Layout
1318
static let paddingS: CGFloat = 8
1419
static let paddingM: CGFloat = 12
@@ -56,6 +61,52 @@ extension ButtonStyle where Self == FrostDestructiveButtonStyle {
5661
static var frostDestructive: FrostDestructiveButtonStyle { FrostDestructiveButtonStyle() }
5762
}
5863

64+
// MARK: - Text field style
65+
66+
extension FrostTheme {
67+
static let inputBackground = Color(.sRGB, red: 0.22, green: 0.22, blue: 0.24, opacity: 1)
68+
}
69+
70+
struct FrostTextFieldStyle: TextFieldStyle {
71+
func _body(configuration: TextField<Self._Label>) -> some View {
72+
configuration
73+
.foregroundStyle(Color.white)
74+
.padding(.horizontal, 8)
75+
.padding(.vertical, 6)
76+
.background(FrostTheme.inputBackground, in: RoundedRectangle(cornerRadius: FrostTheme.cornerRadius))
77+
.overlay(
78+
RoundedRectangle(cornerRadius: FrostTheme.cornerRadius)
79+
.stroke(FrostTheme.divider, lineWidth: 1)
80+
)
81+
}
82+
}
83+
84+
extension TextFieldStyle where Self == FrostTextFieldStyle {
85+
static var frost: FrostTextFieldStyle { FrostTextFieldStyle() }
86+
}
87+
88+
// MARK: - Shared input container modifier
89+
90+
struct FrostInputModifier: ViewModifier {
91+
func body(content: Content) -> some View {
92+
content
93+
.foregroundStyle(Color.white)
94+
.padding(.horizontal, 8)
95+
.padding(.vertical, 6)
96+
.background(FrostTheme.inputBackground, in: RoundedRectangle(cornerRadius: FrostTheme.cornerRadius))
97+
.overlay(
98+
RoundedRectangle(cornerRadius: FrostTheme.cornerRadius)
99+
.stroke(FrostTheme.divider, lineWidth: 1)
100+
)
101+
}
102+
}
103+
104+
extension View {
105+
func frostInputStyle() -> some View {
106+
modifier(FrostInputModifier())
107+
}
108+
}
109+
59110
// MARK: - Status color
60111

61112
extension StatusManager.RipperStatus {

FrostscribeUI/FrostscribeUI/MenuBar/MenuBarIcon.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import FrostscribeCore
33

44
struct MenuBarIcon: View {
55
let status: StatusManager.RipperStatus
6+
@Environment(\.openWindow) private var openWindow
67

78
var body: some View {
89
Image(systemName: symbolName)
910
.foregroundStyle(iconColor)
1011
.symbolEffect(.pulse, isActive: isAnimating)
12+
.onAppear { openWindow(id: "rip-flow") }
1113
}
1214

1315
private var symbolName: String {

FrostscribeUI/FrostscribeUI/MenuBar/MenuBarView.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,24 @@ struct MenuBarView: View {
1111
var body: some View {
1212
VStack(spacing: 0) {
1313
headerRow
14-
Divider()
14+
FrostTheme.divider.frame(height: 1)
1515
StatusSectionView()
1616
.padding(FrostTheme.paddingM)
1717
.contentShape(Rectangle())
1818
.onTapGesture { openSection(.ripJob) }
19-
Divider()
19+
FrostTheme.divider.frame(height: 1)
2020
QueueSectionView()
2121
.padding(FrostTheme.paddingM)
2222
.contentShape(Rectangle())
2323
.onTapGesture { openSection(.encodeQueue) }
24-
Divider()
24+
FrostTheme.divider.frame(height: 1)
2525
footerRow
26-
Divider()
26+
FrostTheme.divider.frame(height: 1)
2727
quitRow
2828
}
2929
.frame(width: FrostTheme.popoverWidth)
30+
.background(FrostTheme.background)
31+
.foregroundStyle(FrostTheme.textPrimary)
3032
}
3133

3234
// MARK: - Navigation
@@ -63,7 +65,7 @@ struct MenuBarView: View {
6365
.frame(width: 7, height: 7)
6466
Text(badgeLabel)
6567
.font(.caption)
66-
.foregroundStyle(.secondary)
68+
.foregroundStyle(FrostTheme.textPrimary.opacity(0.6))
6769
}
6870
}
6971

@@ -98,16 +100,22 @@ struct MenuBarView: View {
98100
.font(.caption)
99101
}
100102
.buttonStyle(.plain)
101-
.foregroundStyle(.secondary)
103+
.foregroundStyle(FrostTheme.textPrimary.opacity(0.6))
102104
Spacer()
103-
Text("v1.0.0")
105+
Text(appVersion)
104106
.font(.caption)
105-
.foregroundStyle(.tertiary)
107+
.foregroundStyle(FrostTheme.textPrimary.opacity(0.35))
106108
}
107109
.padding(.horizontal, FrostTheme.paddingM)
108110
.padding(.vertical, FrostTheme.paddingS)
109111
}
110112

113+
private var appVersion: String {
114+
let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
115+
let b = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
116+
return "v\(v) (\(b))"
117+
}
118+
111119
private var quitRow: some View {
112120
Button {
113121
NSApplication.shared.terminate(nil)
@@ -117,7 +125,7 @@ struct MenuBarView: View {
117125
.frame(maxWidth: .infinity, alignment: .leading)
118126
}
119127
.buttonStyle(.plain)
120-
.foregroundStyle(.secondary)
128+
.foregroundStyle(FrostTheme.textPrimary.opacity(0.6))
121129
.padding(.horizontal, FrostTheme.paddingM)
122130
.padding(.vertical, FrostTheme.paddingS)
123131
}

0 commit comments

Comments
 (0)