Changes
9 changed files (+957/-74)
-
-
@@ -200,6 +200,7 @@ hasScannedForEncodings = 0;knownRegions = ( en, Base, ja, ); mainGroup = C3EABC682DB1170400F786D6; minimizedProjectReferenceProxies = 1;
-
@@ -337,6 +338,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;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 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;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 @@ ConnectionDataModelWrapper?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 @@ UserDefaults.standard.set(token, forKey: StorageKeys.extensionToken)} 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 @@ .frame(maxWidth: .infinity, maxHeight: .infinity).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 @@ List(hierarchies, id: \.self, selection: $browsing.hierarchy) {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 @@ }} } .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 @@ ImageURLBuilderDataModel(host: self.conn.host, port: self.conn.port)) 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 @@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 @@ Task {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 @@ Task {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 @@ Task {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 @@ Task {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 @@ }} } ) { // Label for screen readers. Text("Playback position") Text("PlaybackBar.Seekbar", comment: "A label for seekbar.") } .labelsHidden() .disabled(!(zone.isSeekAllowed ?? false))
-
@@ -159,10 +180,20 @@ Spacer()} 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 @@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 @@ } label: {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 @@ if let onScan = onScan {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 @@ } detail: {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") } } }
-