Compare commits

...

32 Commits

Author SHA1 Message Date
Maxence G
4a5f7d67a0
Better handle on first launch
Improve null check
2023-06-03 22:39:43 +02:00
Maxence G
9be6c8d371
Merge remote-tracking branch 'base/develop' into AddShortcutCapability 2023-06-03 22:26:54 +02:00
birdbird
5a4989186e Merge branch 'fixButtons' into 'develop'
Hide tiny scrollbars on buttons on Android 13

See merge request ultrasonic/ultrasonic!1036
2023-06-03 09:55:17 +00:00
birdbird
eb380b9af9 Merge branch 'r8' into 'develop'
Reenable R8

See merge request ultrasonic/ultrasonic!1026
2023-06-03 09:52:15 +00:00
birdbird
01124c8ecf Update Release.md 2023-06-03 09:37:00 +00:00
birdbird
6a97636c7a [SkipCI] Template 2023-06-03 09:21:51 +00:00
birdbird
53ea17d2b9 [SkipCI] Move template 2023-06-03 09:02:34 +00:00
birdbird
a1e339f850 [skipCI] Update Release.md 2023-06-03 08:59:40 +00:00
birdbird
4809317c63 Add release template -SkipCI 2023-06-03 08:58:47 +00:00
birdbird
c5c0497716 Merge branch 'renovate/major-mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v5

See merge request ultrasonic/ultrasonic!1034
2023-06-02 06:38:28 +00:00
Renovate Bot
79ac73020b Update dependency org.mockito.kotlin:mockito-kotlin to v5 2023-06-01 21:31:52 +00:00
birdbird
d9dfef4016 Merge branch 'playAll' into 'develop'
Fix playing all tracks when the selection has no id

See merge request ultrasonic/ultrasonic!1033
2023-06-01 21:29:26 +00:00
tzugen
3bd3607220
Remove unused fragment param 2023-06-01 23:21:43 +02:00
tzugen
e35a33edde
Use App context when toasting from background tasks,
use App context to resolve error messages
2023-06-01 23:16:17 +02:00
tzugen
c1013f6b80
Fix play all in Track collection random view 2023-06-01 23:16:17 +02:00
birdbird
21a27c691d Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.0

See merge request ultrasonic/ultrasonic!1029
2023-06-01 08:48:58 +00:00
Renovate Bot
25f3ff0bd3 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.0 2023-06-01 08:48:58 +00:00
birdbird
4feb84bd83 Merge branch 'gradle' into 'develop'
Apply assistant changes to gradle file

See merge request ultrasonic/ultrasonic!1032
2023-06-01 08:39:14 +00:00
tzugen
4c049671db
Apply assistant changes to gradle file 2023-06-01 10:31:36 +02:00
birdbird
3a1251dd2a Merge branch 'renovate/gradleplugin' into 'develop'
Update dependency com.android.tools.build:gradle to v8.0.2

See merge request ultrasonic/ultrasonic!1030
2023-06-01 08:01:00 +00:00
birdbird
8dd7758bc6 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request ultrasonic/ultrasonic!1027
2023-06-01 07:59:17 +00:00
gallegonovato
296308cebf
Translated using Weblate (Spanish)
Currently translated at 100.0% (426 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2023-05-29 10:11:15 +02:00
Newson Parker
45ca0966fd
Translated using Weblate (Chinese (Traditional))
Currently translated at 69.0% (294 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/
2023-05-29 10:11:15 +02:00
Newson Parker
77d3f8c11b
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/
2023-05-29 10:11:15 +02:00
birdbird
7a453dbd30 Correct transition reason 2023-05-29 08:11:09 +00:00
Renovate Bot
0a6a12c70a Update dependency com.android.tools.build:gradle to v8.0.2 2023-05-24 19:31:38 +00:00
Óscar García Amor
448fdb70b0 Merge branch 'newid' into 'develop'
Changes applicationId for GitLab builds

See merge request ultrasonic/ultrasonic!1028
2023-05-23 11:02:19 +00:00
Óscar García Amor
5e4ec56ae7
Changes applicationId for GitLab builds 2023-05-23 12:54:54 +02:00
birdbird
8c42700676 Merge branch 'thread' into 'develop'
Ensure correct thread when accepting a rating

See merge request ultrasonic/ultrasonic!1025
2023-05-21 13:08:19 +00:00
tzugen
22fda501f4
Ensure correct thread when accepting a rating 2023-05-21 15:00:41 +02:00
tzugen
556d3bb90d
Reenable R8 2023-05-21 15:00:04 +02:00
tzugen
4e9cea87a8
Hide tiny scrollbars on buttons on Android 13 2023-05-07 11:39:33 +02:00
23 changed files with 165 additions and 62 deletions

View File

@ -74,7 +74,9 @@ Unit Tests:
Assemble Release:
stage: Build
script: ./gradlew assembleRelease
script:
- sed -i 's/applicationId \"org.moire.ultrasonic\"/applicationId "org.moire.ultrasonic.gitlab"/' ultrasonic/build.gradle
- ./gradlew assembleRelease
artifacts:
name: ultrasonic-release-unsigned-${CI_COMMIT_SHA}
paths:

View File

@ -0,0 +1,10 @@
#### Before merge:
- [ ] MR is targetting the master branch
- [ ] **Squash commits must be disabled!**
- [ ] RoboTests (5 physical, 10 virtual) on a Release apk return no errors
- [ ] Release notes present
#### After merge
- [ ] ``git fetch``
- [ ] Create an annotated and signed tag: ``git tag -sa``
- [ ] Push the tag to git:``git push --tags``

View File

@ -52,7 +52,11 @@ style:
active: true
ForbiddenComment:
active: true
values: ['FIXME:', 'STOPSHIP:']
comments:
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
value: 'FIXME:'
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
value: 'STOPSHIP:'
WildcardImport:
active: true
MaxLineLength:

View File

@ -21,5 +21,5 @@ android.nonFinalResIds=true
org.gradle.unsafe.configuration-cache=true
# TODO Renable on day (check that Retrofit, Jackson, and Imageloader are working)
android.enableR8.fullMode=false
android.enableR8.fullMode=true

View File

@ -3,11 +3,11 @@
gradle = "8.1.1"
navigation = "2.5.3"
gradlePlugin = "8.0.1"
gradlePlugin = "8.0.2"
androidxcore = "1.10.1"
ktlint = "0.43.2"
ktlintGradle = "11.3.2"
detekt = "1.22.0"
detekt = "1.23.0"
preferences = "1.2.0"
media3 = "1.0.2"
@ -31,7 +31,7 @@ picasso = "2.8"
junit4 = "4.13.2"
junit5 = "5.9.3"
mockito = "5.3.1"
mockitoKotlin = "4.1.0"
mockitoKotlin = "5.0.0"
kluent = "1.73"
apacheCodecs = "1.15"
robolectric = "4.10.3"

View File

@ -14,7 +14,7 @@ android {
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
resConfigs 'cs', 'de', 'en', 'es', 'fr', 'gl', 'hu', 'it', 'ja', 'nb-rNO', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW'
resourceConfigurations += ['cs', 'de', 'en', 'es', 'fr', 'gl', 'hu', 'it', 'ja', 'nb-rNO', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW']
}
bundle.language.enableSplit = false

View File

@ -1,5 +1,4 @@
#### From Jackson
-keepattributes *Annotation*,EnclosingMethod,Signature
-keepnames class com.fasterxml.jackson.** {
*;

View File

@ -1,8 +1,14 @@
-dontobfuscate
### Don't remove subsonic api serializers/entities
-keep class org.moire.ultrasonic.api.subsonic.response.** { *; }
-keep class org.moire.ultrasonic.api.subsonic.models.** { *; }
-keep class org.moire.ultrasonic.api.subsonic.** { *; }
## Don't remove the domain models
-keep class org.moire.ultrasonic.domain.** { *; }
## Don't remove the imageloader
-keep class org.moire.ultrasonic.imageloader.** { *; }
-keep class org.moire.ultrasonic.provider.AlbumArtContentProvider { *; }
## Don't remove NowPlayingFragment
-keep class org.moire.ultrasonic.fragment.NowPlayingFragment { *; }

View File

@ -58,7 +58,7 @@ public abstract class BackgroundTask<T> implements ProgressListener
protected String getErrorMessage(Throwable error)
{
return CommunicationError.getErrorMessage(error, activity);
return CommunicationError.getErrorMessage(error);
}
@Override

View File

@ -286,7 +286,8 @@ class NavigationActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun setupAppShortcut() {
// TODO: Handle adding shortcut only if player is ready to add songs (has any servers setup)
if (UApp.instance!!.isFirstRun)
return
val shortcutIntent = Intent(this, NavigationActivity::class.java).apply {
action = Constants.INTENT_PLAY_RANDOM_SONGS
}
@ -398,7 +399,7 @@ class NavigationActivity : AppCompatActivity() {
if (intent == null) return
if (intent.action == Constants.INTENT_PLAY_RANDOM_SONGS) {
val currentFragment = host!!.childFragmentManager.fragments.last()
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
val downloadHandler: DownloadHandler by inject()

View File

@ -142,13 +142,15 @@ class TrackViewHolder(val view: View) :
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
// Ignore updates which are not for the current song
if (it.id != song.id) return@subscribe
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateSingleStar(it.rating.isHeart)
} else if (it.rating is StarRating) {
updateFiveStars(it.rating.starRating.toInt())
if (it.rating is HeartRating) {
updateSingleStar(it.rating.isHeart)
} else if (it.rating is StarRating) {
updateFiveStars(it.rating.starRating.toInt())
}
}
}
}

View File

@ -401,7 +401,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
Timber.w(exception)
ErrorDialog.Builder(requireContext())
.setTitle(R.string.error_label)
.setMessage(getErrorMessage(exception, context))
.setMessage(getErrorMessage(exception))
.show()
}
}

View File

@ -78,6 +78,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
@ -662,7 +663,6 @@ class PlayerFragment :
parentId = track.parent,
isAlbum = true
)
findNavController().navigate(action)
return true
}
@ -822,16 +822,16 @@ class PlayerFragment :
musicService.createPlaylist(null, playlistName, entries)
}.invokeOnCompletion {
if (it == null || it is CancellationException) {
Util.toast(context, R.string.download_playlist_done)
Util.toast(UApp.applicationContext(), R.string.download_playlist_done)
} else {
Timber.e(it, "Exception has occurred in savePlaylistInBackground")
val msg = String.format(
Locale.ROOT,
"%s %s",
resources.getString(R.string.download_playlist_error),
CommunicationError.getErrorMessage(it, context)
CommunicationError.getErrorMessage(it)
)
Util.toast(context, msg)
Util.toast(UApp.applicationContext(), msg)
}
}
}

View File

@ -351,13 +351,11 @@ open class TrackCollectionFragment(
val isArtist = navArgs.isArtist
// Need a valid id to download stuff
val id = navArgs.id ?: return
if (hasSubFolders) {
// Need a valid id to recurse sub directories stuff
if (hasSubFolders && navArgs.id != null) {
downloadHandler.fetchTracksAndAddToController(
fragment = this,
id = id,
id = navArgs.id!!,
append = append,
autoPlay = !append,
shuffle = shuffle,

View File

@ -505,7 +505,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
it.onMediaItemTransition(
currentMedia,
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
)
}
}

View File

@ -14,6 +14,6 @@ data class PlaybackState(
var repeatMode: Int = 0
) : Serializable {
companion object {
const val serialVersionUID = -293487987L
private const val serialVersionUID = -293487987L
}
}

View File

@ -46,7 +46,7 @@ class DownloadHandler(
var successString: String? = null
// Launch the Job
executeTaskWithToast(fragment, {
executeTaskWithToast({
val tracksToDownload: List<Track> = tracks
?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare)
@ -104,7 +104,7 @@ class DownloadHandler(
) {
var successString: String? = null
// Launch the Job
executeTaskWithToast(fragment, {
executeTaskWithToast({
val songs: MutableList<Track> =
getTracksFromServer(isArtist, id, isDirectory, name, isShare)

View File

@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
import timber.log.Timber
@ -46,14 +47,14 @@ object CommunicationError {
ErrorDialog(
context = context,
message = getErrorMessage(error, context)
message = getErrorMessage(error)
).show()
}
@JvmStatic
@Suppress("ReturnCount")
fun getErrorMessage(error: Throwable, context: Context?): String {
if (context == null) return "Couldn't get Error message, Context is null"
fun getErrorMessage(error: Throwable): String {
val context = UApp.applicationContext()
if (error is IOException && !Util.hasUsableNetwork()) {
return context.resources.getString(R.string.background_task_no_network)
} else if (error is FileNotFoundException) {

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import timber.log.Timber
object CoroutinePatterns {
@ -30,7 +31,6 @@ object CoroutinePatterns {
}
fun CoroutineScope.executeTaskWithToast(
fragment: Fragment,
task: suspend CoroutineScope.() -> Unit,
successString: () -> String?
): Job {
@ -40,7 +40,7 @@ fun CoroutineScope.executeTaskWithToast(
// Setup a handler when the job is done
job.invokeOnCompletion {
val toastString = if (it != null && it !is CancellationException) {
CommunicationError.getErrorMessage(it, fragment.context)
CommunicationError.getErrorMessage(it)
} else {
successString()
}
@ -49,7 +49,7 @@ fun CoroutineScope.executeTaskWithToast(
if (toastString == null) return@invokeOnCompletion
launch(Dispatchers.Main) {
Util.toast(fragment.context, toastString)
Util.toast(UApp.applicationContext(), toastString)
}
}
@ -62,7 +62,7 @@ fun CoroutineScope.executeTaskWithModalDialog(
successString: () -> String
) {
// Create the job
val job = executeTaskWithToast(fragment, task, successString)
val job = executeTaskWithToast(task, successString)
// Create the dialog
val builder = InfoDialog.Builder(fragment.requireContext())

View File

@ -7,7 +7,7 @@
android:orientation="horizontal"
android:padding="6dp" >
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_select"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -20,7 +20,7 @@
app:iconGravity="textEnd"
app:iconSize="26dp" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_play_now"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -31,9 +31,10 @@
android:contentDescription="@string/common.play_now"
app:icon="@drawable/media_start"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_play_next"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -44,9 +45,10 @@
android:contentDescription="@string/common.play_next"
app:icon="@drawable/ic_play_next"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_play_last"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -57,9 +59,11 @@
android:contentDescription="@string/common.play_last"
app:icon="@drawable/ic_play_last"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_pin"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -70,9 +74,11 @@
android:contentDescription="@string/common.pin"
app:icon="@drawable/ic_menu_pin"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_unpin"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -83,9 +89,11 @@
android:contentDescription="@string/common.unpin"
app:icon="@drawable/ic_menu_unpin"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_download"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -96,9 +104,11 @@
android:contentDescription="@string/common.download"
app:icon="@drawable/ic_menu_download"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_delete"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -109,9 +119,11 @@
android:contentDescription="@string/common.delete"
app:icon="@drawable/ic_menu_close"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_more"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -122,6 +134,8 @@
android:contentDescription="@string/search.more"
app:icon="@drawable/media_forward"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
</LinearLayout>

View File

@ -452,4 +452,5 @@
<string name="supported_server_features">Funciones soportadas</string>
<string name="foreground_exception_title">No se puede reanudar la reproducción</string>
<string name="foreground_exception_text">Presione el botón de reproducción en la notificación de medios si aún está presente; de lo contrario, abra la aplicación para iniciar la reproducción y vuelva a conectar la sesión al controlador</string>
<string name="settings.max_bitrate_pinning">Tasa de bits máxima: al fijar una canción de forma permanente</string>
</resources>

View File

@ -67,7 +67,7 @@
<string name="download.menu_save">保存播放列表</string>
<string name="download.menu_screen_off">关闭屏幕常亮</string>
<string name="download.menu_screen_on">开启屏幕常亮</string>
<string name="download.menu_show_album">显示专辑</string>
<string name="download.menu_show_album">转到专辑</string>
<string name="download.menu_shuffle">随机</string>
<string name="download.menu_shuffle_on">随机播放模式已启用</string>
<string name="download.menu_shuffle_off">随机播放模式已禁用</string>
@ -280,7 +280,7 @@
<string name="settings.test_connection_title">测试连接</string>
<string name="settings.theme_light">亮色</string>
<string name="settings.theme_dark">暗色</string>
<string name="settings.theme_black">Black</string>
<string name="settings.theme_black">黑色</string>
<string name="settings.theme_title">主题</string>
<string name="settings.title.allow_self_signed_certificate">允许自签名 HTTPS 证书</string>
<string name="settings.title.force_plain_text_password">强制原始密码认证</string>
@ -337,7 +337,7 @@
<string name="share_default_greeting">看看我从 %s 分享的这首音乐</string>
<string name="share_via">分享歌曲通过</string>
<string name="menu.share">分享</string>
<string name="download.menu_show_artist">显示艺术家</string>
<string name="download.menu_show_artist">转到艺术家</string>
<string name="common_multiple_years">数年</string>
<string name="settings.debug.title">调试选项</string>
<string name="settings.debug.log_to_file">将调试日志写入文件</string>
@ -448,4 +448,5 @@
<string name="language.cs">捷克语</string>
<string name="language.de">德语</string>
<string name="language.pt_BR">葡萄牙语(巴西)</string>
<string name="settings.max_bitrate_pinning">最大比特率 - 永久固定歌曲时</string>
</resources>

View File

@ -84,7 +84,7 @@
<string name="settings.increment_time_8">8 秒</string>
<string name="settings.custom_cache_location">使用自訂緩衝路徑</string>
<string name="settings.cache_location">緩衝路径</string>
<string name="settings.cache_location_error">錯誤緩衝路徑,使用預設緩衝路徑</string>
<string name="settings.cache_location_error">錯誤緩衝路徑,使用預設緩衝路徑</string>
<string name="settings.cache_size">緩衝大小</string>
<string name="settings.cache_size_100">100 MB</string>
<string name="settings.cache_size_1000">1 GB</string>
@ -129,7 +129,7 @@
<string name="time_span_disabled">已停用</string>
<string name="share_comment">註記</string>
<string name="server_menu.delete">刪除</string>
<string name="download.menu_show_album">顯示專輯</string>
<string name="download.menu_show_album">轉至專輯</string>
<string name="language.zh_CN">簡體中文(中國)</string>
<string name="download.menu_save">儲存播放清單</string>
<string name="download.bookmark_set_at_position" formatted="false">書籤設置在 %s。</string>
@ -232,4 +232,68 @@
<string name="settings.hide_media_toast">在 Android 系統下次掃描裝置內音樂時生效。</string>
<string name="settings.download_transition">播放時顯示正在播放介面</string>
<string name="settings.download_transition_summary">在媒體庫介面開始播放後切換到正在播放介面</string>
<string name="settings.search_50">50</string>
<string name="settings.preload_3">3 首歌</string>
<string name="settings.search_1">1</string>
<string name="settings.search_20">20</string>
<string name="settings.search_75">75</string>
<string name="settings.share_minutes">分鐘</string>
<string name="settings.preload_500">500 首歌</string>
<string name="settings.max_bitrate_112">112 Kbps</string>
<string name="settings.preload_1">1 首歌</string>
<string name="settings.search_3">3</string>
<string name="settings.search_40">40</string>
<string name="settings.search_500">500</string>
<string name="settings.max_bitrate_160">160 Kbps</string>
<string name="util.zero_time">0:00</string>
<string name="settings.network_timeout_120000">120 秒</string>
<string name="settings.share_hours">小時</string>
<string name="settings.theme_black">黑色</string>
<string name="server_editor.authentication">認證</string>
<string name="settings.preload_2">2 首歌</string>
<string name="settings.search_10">10</string>
<string name="settings.server_address">伺服器地址</string>
<string name="settings.network_timeout_60000">60 秒</string>
<string name="settings.search_5">5</string>
<string name="settings.preload_100">100 首歌</string>
<string name="settings.theme_light">明色</string>
<string name="settings.max_bitrate_96">96 Kbps</string>
<string name="util.bytes_format.kilobyte">0 KB</string>
<string name="settings.override_language">覆寫當前語言</string>
<string name="settings.preload_5">5 首歌</string>
<string name="settings.search_250">250</string>
<string name="settings.max_bitrate_192">192 Kbps</string>
<string name="settings.max_bitrate_80">80 Kbps</string>
<string name="settings.search_25">25</string>
<string name="settings.search_30">30</string>
<string name="settings.search_100">100</string>
<string name="main.video" tools:ignore="UnusedResources">影片</string>
<string name="time_span_disable">禁用</string>
<string name="settings.preload_50">50 首歌</string>
<string name="song_details.kbps">%d kbps</string>
<string name="settings.preload_10">10 首歌</string>
<string name="settings.max_bitrate_256">256 Kbps</string>
<string name="settings.max_bitrate_32">32 Kbps</string>
<string name="settings.max_bitrate_320">320 Kbps</string>
<string name="settings.max_bitrate_64">64 Kbps</string>
<string name="settings.network_timeout">網路延時</string>
<string name="settings.network_timeout_105000">105 秒</string>
<string name="settings.network_timeout_45000">45 秒</string>
<string name="settings.network_timeout_75000">75 秒</string>
<string name="settings.network_timeout_90000">90 秒</string>
<string name="settings.notifications_title">通知</string>
<string name="settings.network_title">網路</string>
<string name="settings.other_title">其他設定</string>
<string name="settings.search_15">15</string>
<string name="settings.server_color">伺服器顏色</string>
<string name="util.bytes_format.byte">0 B</string>
<string name="util.bytes_format.gigabyte">0.00 GB</string>
<string name="settings.server_username">用戶名</string>
<string name="settings.theme_dark">暗色</string>
<string name="util.no_time" tools:ignore="TypographyDashes">-:--</string>
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="settings.preload_1000">1000 首歌</string>
<string name="settings.server_password">密碼</string>
<string name="settings.share_days"></string>
<string name="settings.max_bitrate_128">128 Kbps</string>
</resources>