Changes
9 changed files (+957/-74)
-
-
@@ -200,6 +200,7 @@knownRegions = ( en, Base, ja, ); mainGroup = C3EABC682DB1170400F786D6; minimizedProjectReferenceProxies = 1;
-
@@ -337,6 +338,7 @@MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug;
-
@@ -392,6 +394,7 @@MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; }; name = Release; };
-
-
-
@@ -0,0 +1,773 @@{ "sourceLanguage" : "en", "strings" : { "AppTitle" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Plac" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Plac" } } } }, "Browser.Error.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "An error occurred when loading the page." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ページの読込中にエラーが発生しました。" } } } }, "Browser.Error.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Failed to load" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "読み込み失敗" } } } }, "Browser.Error.Retry" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Retry" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再読み込み" } } } }, "ConnectedScene.Category.Albums" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Albums" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アルバム" } } } }, "ConnectedScene.Category.Artists" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Artists" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アーティスト" } } } }, "ConnectedScene.Category.Composers" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Composers" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "作曲者" } } } }, "ConnectedScene.Category.Explore" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Explore" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ホーム" } } } }, "ConnectedScene.Category.Genres" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Genres" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ジャンル" } } } }, "ConnectedScene.Category.InternetRadio" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Internet Radios" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インターネットラジオ" } } } }, "ConnectedScene.Category.Playlists" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Playlists" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プレイリスト" } } } }, "ConnectedScene.Category.Search" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Search" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索" } } } }, "ConnectedScene.Category.Settings" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Settings" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "設定" } } } }, "ConnectedScene.ServerMessage.Dismiss" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Dismiss" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "閉じる" } } } }, "ConnectedScene.ServerMessage.Title" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Message from Roon server" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Roon サーバからのメッセージ" } } } }, "ConnectionScreen.Cancelled.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Cancelled connecting to Roon server." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ユーザによって Roon サーバへの接続がキャンセルされました。" } } } }, "ConnectionScreen.Cancelled.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Connection cancelled" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "接続キャンセル" } } } }, "ConnectionScreen.Disconnect" : { "comment" : "Button label for aborting connection process.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Close" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "閉じる" } } } }, "ConnectionScreen.Error.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Failed to connect due to an error." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "エラーが発生したため Roon サーバへ接続できませんでした。" } } } }, "ConnectionScreen.Error.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Failed to connect" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "接続エラー" } } } }, "ConnectionScreen.Loading.Cancel" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Cancel" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "キャンセル" } } } }, "ConnectionScreen.Loading.Label" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Connecting to Roon Server.\nMake sure you granted access at Settings > Extension page in official Roon client." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Roon サーバに接続しています。 Roon 公式アプリケーションの “設定 > 拡張” ページで Plac を承認してください。" } } } }, "ConnectionScreen.NotFound.Description(id=%@)" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Server (ID: %@) does not found on network." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Roon サーバ (ID: %@) がネットワーク上に見つかりませんでした。" } } } }, "ConnectionScreen.NotFound.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Server not found" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "見つかりません" } } } }, "ConnectionScreen.Reconnect" : { "comment" : "Button label for connecting to Roon server from non-connected state.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Reconnect" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再接続" } } } }, "Menu.Disconnect" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Disconnect" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サーバから切断" } } } }, "PlaybackBar.Next" : { "comment" : "A label for a next button.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Next" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "次へ" } } } }, "PlaybackBar.Pause" : { "comment" : "A label for a pause button.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Pause" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一時停止" } } } }, "PlaybackBar.Play" : { "comment" : "A label for a play button.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Play" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再生" } } } }, "PlaybackBar.Previous" : { "comment" : "A label for a previous button.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Previous" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "前へ" } } } }, "PlaybackBar.Seekbar" : { "comment" : "A label for seekbar.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Playback seekbar" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再生バー" } } } }, "PlaybackBar.ZonePicker" : { "comment" : "A label for zone picker.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Zone" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ゾーン" } } } }, "ServerDiscovery.Empty.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "No Roon server found on local network." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ネットワーク上に Roon サーバが見つかりませんでした。" } } } }, "ServerDiscovery.Empty.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "No server found" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "見つかりません" } } } }, "ServerDiscovery.Error.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "An error occurred during a scan." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索中にエラーが発生しました。" } } } }, "ServerDiscovery.Error.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Scan failed" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索失敗" } } } }, "ServerDiscovery.Host" : { "comment" : "A label for server host field.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "IP Address" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アドレス" } } } }, "ServerDiscovery.Loading.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Scanning Roon servers on local network." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ネットワーク上にある Roon サーバを検索しています。" } } } }, "ServerDiscovery.Loading.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Scanning" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索中" } } } }, "ServerDiscovery.NetworkError.Description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Failed to scan Roon servers due to network error." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索中にネットワークの問題が発生しました。" } } } }, "ServerDiscovery.NetworkError.Heading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Scan failed" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索失敗" } } } }, "ServerDiscovery.Open" : { "comment" : "Label for a button that starts a connection.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Open" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "開く" } } } }, "ServerDiscovery.Placeholder" : { "comment" : "Placeholder text for server details view.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Select server from list" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サーバを選んでください" } } } }, "ServerDiscovery.Refresh" : { "comment" : "Icon label for refresh button.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Refresh" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新" } } } }, "ServerDiscovery.RefreshHelp" : { "comment" : "Help tooltip text for refresh button.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Refresh server list" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リストを更新する" } } } }, "ServerDiscovery.Title" : { "comment" : "Title of server discovery scene. Not visible on macOS.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Servers" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サーバ一覧" } } } }, "ServerDiscovery.Version" : { "comment" : "A label for server version field.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Version" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バージョン" } } } } }, "version" : "1.0" }
-
-
-
@@ -0,0 +1,15 @@Copyright 2025 Shota FUJI Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. SPDX-License-Identifier: Apache-2.0
-
-
-
@@ -37,8 +37,14 @@ private struct ConnectionCommands: Commands {var body: some Commands { CommandGroup(after: .appInfo) { Button("Disconnect", systemImage: "powercode") { Button { model?.model = nil } label: { Label { Text("Menu.Disconnect") } icon: { Image(systemName: "powercode") } } .disabled(model?.model == nil) }
-
@@ -108,7 +114,7 @@ struct PlacApp: App {} var body: some Scene { WindowGroup("Plac", id: "main-window") { WindowGroup(Text("AppTitle"), id: "main-window") { if let model = model.model { ConnectionScreen( model: model,
-
-
-
@@ -41,12 +41,18 @@ struct Browser: View {.background(.background) case .some(.failed(_)): ContentUnavailableView { Label("Load error", systemImage: "exclamationmark.triangle") Label { Text("Browser.Error.Heading") } icon: { Image(systemName: "exclamationmark.triangle") } } description: { Text("An error occurred when loading the page.") Text("Browser.Error.Description") } actions: { Button("Retry") { Button { model.reload() } label: { Text("Browser.Error.Retry") } } case .some(.loaded(let list, let groups)):
-
-
-
@@ -59,23 +59,23 @@ struct ConnectedScreen: View {hierarchy in switch hierarchy { case .browse: Text("Explore") Text("ConnectedScene.Category.Explore") case .playlists: Text("Playlists") Text("ConnectedScene.Category.Playlists") case .settings: Text("Settings") Text("ConnectedScene.Category.Settings") case .albums: Text("Albums") Text("ConnectedScene.Category.Albums") case .artists: Text("Artists") Text("ConnectedScene.Category.Artists") case .genres: Text("Genres") Text("ConnectedScene.Category.Genres") case .composers: Text("Composers") Text("ConnectedScene.Category.Composers") case .search: Text("Search") Text("ConnectedScene.Category.Search") case .internetRadio: Text("Internet Radio") Text("ConnectedScene.Category.InternetRadio") } } } detail: {
-
@@ -89,11 +89,11 @@ struct ConnectedScreen: View {} } .alert( "Message from server", Text("ConnectedScene.ServerMessage.Title"), isPresented: shouldDisplayMessage, presenting: browsing.message ) { _ in Button("Close") { Button("ConnectedScene.ServerMessage.Dismiss") { browsing.message = nil } } message: { message in
-
-
-
@@ -49,65 +49,77 @@ struct ConnectionScreen: View {) case .failed(RoonKit.ServerLookupError.notFound): ContentUnavailableView { Label( "Server not found", systemImage: "exclamationmark.magnifyingglass" ) Label { Text("ConnectionScreen.NotFound.Heading") } icon: { Image(systemName: "exclamationmark.magnifyingglass") } } description: { Text("Server of ID=\(self.conn.serverID) does not found.") Text( "ConnectionScreen.NotFound.Description(id=\(self.conn.serverID))" ) } actions: { Button(role: .cancel) { onDisconnect() } label: { Text("Cancel") Text( "ConnectionScreen.Disconnect", comment: "Button label for aborting connection process." ) } Button { onReconnect() } label: { Text("Retry") Text( "ConnectionScreen.Reconnect", comment: "Button label for connecting to Roon server from non-connected state." ) } } case .failed(CancelError.cancelled): ContentUnavailableView { Label( "Connection cancelled", systemImage: "exclamationmark.magnifyingglass" ) Label { Text("ConnectionScreen.Cancelled.Heading") } icon: { Image(systemName: "exclamationmark.magnifyingglass") } } description: { Text("Cancelled connection to Roon server.") Text("ConnectionScreen.Cancelled.Description") } actions: { Button(role: .cancel) { onDisconnect() } label: { Text("Close") Text("ConnectionScreen.Disconnect") } Button { onReconnect() } label: { Text("Connect") Text("ConnectionScreen.Reconnect") } } case .failed(_): ContentUnavailableView { Label( "Failed to connect", systemImage: "exclamationmark.magnifyingglass" ) Label { Text("ConnectionScreen.Error.Heading") } icon: { Image(systemName: "exclamationmark.magnifyingglass") } } description: { Text("Unable to connect to server due to error.") Text("ConnectionScreen.Error.Description") } actions: { Button(role: .cancel) { onDisconnect() } label: { Text("Cancel") Text("ConnectionScreen.Disconnect") } Button { onReconnect() } label: { Text("Connect") Text("ConnectionScreen.Reconnect") } } }
-
@@ -125,16 +137,14 @@ private struct Loading: View {var body: some View { VStack(spacing: 8) { ProgressView { Text( "Connecting to Roon Server.\nMake sure you granted access at Settings > Extension page in official Roon client." ) .multilineTextAlignment(.center) Text("ConnectionScreen.Loading.Label") .multilineTextAlignment(.center) } Button(role: .cancel) { onCancel?() } label: { Text("Cancel") Text("ConnectionScreen.Loading.Cancel") } } }
-
-
-
@@ -59,7 +59,14 @@ struct PlaybackBar: View {await performAction(.previous) } } label: { Label("Previous", systemImage: "backward.end.fill") Label { Text( "PlaybackBar.Previous", comment: "A label for a previous button." ) } icon: { Image(systemName: "backward.end.fill") } } .font(.title3) .disabled(isPerformingAction)
-
@@ -71,7 +78,14 @@ struct PlaybackBar: View {await performAction(.pause) } } label: { Label("Pause", systemImage: "pause.fill") Label { Text( "PlaybackBar.Pause", comment: "A label for a pause button." ) } icon: { Image(systemName: "pause.fill") } } .font(.title) .disabled(isPerformingAction)
-
@@ -83,7 +97,11 @@ struct PlaybackBar: View {await performAction(.play) } } label: { Label("Play", systemImage: "play.fill") Label { Text("PlaybackBar.Play", comment: "A label for a play button.") } icon: { Image(systemName: "play.fill") } } .font(.title) .disabled(isPerformingAction)
-
@@ -96,7 +114,11 @@ struct PlaybackBar: View {await performAction(.next) } } label: { Label("Next", systemImage: "forward.end.fill") Label { Text("PlaybackBar.Next", comment: "A label for a next button.") } icon: { Image(systemName: "forward.end.fill") } } .font(.title3) .disabled(isPerformingAction)
-
@@ -148,8 +170,7 @@ struct PlaybackBar: View {} } ) { // Label for screen readers. Text("Playback position") Text("PlaybackBar.Seekbar", comment: "A label for seekbar.") } .labelsHidden() .disabled(!(zone.isSeekAllowed ?? false))
-
@@ -159,10 +180,20 @@ struct PlaybackBar: View {} if model.zones.count > 0 && zone != nil { Picker("Zone", selection: $model.zone) { Picker(selection: $model.zone) { ForEach(model.zones) { (zone: TransportService.Zone) in Text(zone.displayName).tag(zone) } } label: { Label { Text( "PlaybackBar.ZonePicker", comment: "A label for zone picker." ) } icon: { Image(systemName: "hifispeaker") } .labelStyle(.iconOnly) } .scaledToFit() }
-
-
-
@@ -104,24 +104,33 @@ private struct PureView: View {Button { onConnectAction?(server) } label: { Text("Open") Text( "ServerDiscovery.Open", comment: "Label for a button that starts a connection." ) } .buttonStyle(.borderedProminent) } VStack(alignment: .leading) { Text("Version") .font(.headline) Text( "ServerDiscovery.Version", comment: "A label for server version field." ) .font(.headline) Text(server.version) .font(.subheadline) } VStack(alignment: .leading) { Text("IP address") .font(.headline) Text( "ServerDiscovery.Host", comment: "A label for server host field." ) .font(.headline) Text("\(server.host):\(String(server.port, radix: 10))") Text(verbatim: "\(server.host):\(String(server.port, radix: 10))") .font(.subheadline) }
-
@@ -132,7 +141,12 @@ private struct PureView: View {Text(server.name) } } .navigationTitle("Servers") .navigationTitle( Text( "ServerDiscovery.Title", comment: "Title of server discovery scene. Not visible on macOS." ) ) .toolbar { ToolbarItem(placement: .navigation) { Button {
-
@@ -140,9 +154,21 @@ private struct PureView: View {onScan() } } label: { Label("Reload", systemImage: "arrow.trianglehead.clockwise") Label { Text( "ServerDiscovery.Refresh", comment: "Icon label for refresh button." ) } icon: { Image(systemName: "arrow.trianglehead.clockwise") } } .help("Reload server list") .help( Text( "ServerDiscovery.RefreshHelp", comment: "Help tooltip text for refresh button." ) ) .disabled(busy) } }
-
@@ -150,41 +176,54 @@ private struct PureView: View {switch status { case .loading: ContentUnavailableView { Label("Scanning", systemImage: "waveform.badge.magnifyingglass") .symbolEffect(.variableColor) Label { Text("ServerDiscovery.Loading.Heading") } icon: { Image(systemName: "waveform.badge.magnifyingglass") } .symbolEffect(.variableColor) } description: { Text("Scanning Roon servers on local network.") Text("ServerDiscovery.Loading.Description") } case .loaded: if servers.isEmpty { ContentUnavailableView { Label("No servers", systemImage: "square.dashed") Label { Text("ServerDiscovery.Empty.Heading") } icon: { Image(systemName: "square.dashed") } } description: { Text("No Roon server found on local network.") Text("ServerDiscovery.Empty.Description") } } else { Text("Select a server from list") Text( "ServerDiscovery.Placeholder", comment: "Placeholder text for server details view." ) } case .failed(let error): switch error { case RoonKit.ServerLookupError.socketError(_), RoonKit.ServerLookupError.connectionClosed: ContentUnavailableView { Label( "Unable to scan", systemImage: "wifi.exclamationmark" ) Label { Text("ServerDiscovery.NetworkError.Heading") } icon: { Image(systemName: "wifi.exclamationmark") } } description: { Text("Failed to scan Roon servers due to network error.") Text("ServerDiscovery.NetworkError.Description") } default: ContentUnavailableView { Label( "Unable to scan", systemImage: "exclamationmark.magnifyingglass" ) Label { Text("ServerDiscovery.Error.Heading") } icon: { Image(systemName: "exclamationmark.magnifyingglass") } } description: { Text("An error occurred during a scan operation.") Text("ServerDiscovery.Error.Description") } } }
-