build: i18n module.

This commit is contained in:
oxy
2023-10-14 01:14:31 +08:00
parent fb4c021e1b
commit c983f14ea1
73 changed files with 449 additions and 370 deletions

1
.idea/gradle.xml generated
View File

@ -23,6 +23,7 @@
<option value="$PROJECT_DIR$/features/live" />
<option value="$PROJECT_DIR$/features/main" />
<option value="$PROJECT_DIR$/features/setting" />
<option value="$PROJECT_DIR$/i18n" />
<option value="$PROJECT_DIR$/lint" />
<option value="$PROJECT_DIR$/ui" />
</set>

View File

@ -1,5 +1,12 @@
# M3UAndroid
![](docs/img/feat_live_3.png)
### 📢 Translations Wanted 📢
Please submit a pull request if you want to help with translation.
App strings ([ENGLISH](i18n/src/main/res/values), [Simplified Chinese](i18n/src/main/res/values-zh-rCN))
### Features
- [x] M3U and M3U8 files.

View File

@ -28,7 +28,7 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:label="@string/ui_app_name"
android:supportsRtl="true"
android:theme="@style/Theme.M3U.Starting"
android:usesCleartextTraffic="true"
@ -54,7 +54,7 @@
<activity
android:name="com.m3u.features.crash.CrashActivity"
android:label="@string/label_crash"
android:label="@string/app_label_crash"
android:launchMode="singleInstance" />
<provider

View File

@ -14,10 +14,10 @@ import com.m3u.features.console.navigation.consoleScreen
import com.m3u.features.feed.navigation.feedScreen
import com.m3u.features.live.navigation.livePlaylistScreen
import com.m3u.features.live.navigation.liveScreen
import com.m3u.features.main.R
import com.m3u.ui.Destination
import com.m3u.ui.Navigate
import com.m3u.ui.model.LocalHelper
import com.m3u.i18n.R as I18R
@Composable
fun M3UNavHost(
@ -42,7 +42,7 @@ fun M3UNavHost(
onCurrentPage = onCurrentPage,
navigateToFeed = { feed ->
helper.title = feed.title.ifEmpty {
if (feed.local) context.getString(R.string.imported_feed_title)
if (feed.local) context.getString(I18R.string.feat_main_imported_feed_title)
else ""
}
navigate(Destination.Feed(feed.url))

View File

@ -1,4 +0,0 @@
<resources>
<string name="label_crash">应用发生崩溃</string>
<string name="features_scheme_import">导入频道</string>
</resources>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label_crash">app crashes</string>
<string name="features_scheme_import">Import as live</string>
</resources>

View File

@ -35,6 +35,7 @@ android {
dependencies {
lintPublish(project(":lint"))
lintChecks(project(":lint"))
api(project(":i18n"))
implementation(libs.androidx.core.core.ktx)
implementation(libs.androidx.appcompat.appcompat)

View File

@ -15,7 +15,6 @@ import com.m3u.core.wrapper.emitException
import com.m3u.core.wrapper.emitMessage
import com.m3u.core.wrapper.emitProgress
import com.m3u.core.wrapper.emitResource
import com.m3u.data.R
import com.m3u.data.database.dao.FeedDao
import com.m3u.data.database.dao.LiveDao
import com.m3u.data.database.entity.Feed
@ -35,6 +34,7 @@ import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import javax.inject.Inject
import com.m3u.i18n.R as I18R
class FeedRepositoryImpl @Inject constructor(
private val feedDao: FeedDao,
@ -124,7 +124,7 @@ class FeedRepositoryImpl @Inject constructor(
liveDao.insert(live)
emitResource(Unit)
} catch (e: FileNotFoundException) {
error(context.getString(R.string.error_file_not_found))
error(context.getString(I18R.string.data_error_file_not_found))
} catch (e: Exception) {
logger.log(e)
emitException(e)

View File

@ -17,6 +17,7 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.supervisorScope
import com.m3u.i18n.R as I18R
@HiltWorker
class SubscriptionInBackgroundWorker @AssistedInject constructor(
@ -33,7 +34,7 @@ class SubscriptionInBackgroundWorker @AssistedInject constructor(
url ?: return@coroutineScope Result.failure()
createChannel()
if (title.isEmpty()) {
val message = context.getString(R.string.error_empty_title)
val message = context.getString(I18R.string.data_error_empty_title)
val data = workDataOf("message" to message)
failure(message)
Result.failure(data)

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_file_not_found">文件不存在</string>
<string name="error_empty_title">订阅名称为空</string>
</resources>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_file_not_found">file not found</string>
<string name="error_empty_title">playlist name is empty</string>
</resources>

View File

@ -19,6 +19,7 @@ import com.m3u.ui.components.MonoText
import com.m3u.ui.model.LocalHelper
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.repeatOnLifecycle
import com.m3u.i18n.R as I18R
@Composable
internal fun AboutRoute(
@ -26,7 +27,7 @@ internal fun AboutRoute(
viewModel: AboutViewModel = hiltViewModel()
) {
val helper = LocalHelper.current
val title = stringResource(R.string.about_title)
val title = stringResource(I18R.string.feat_about_title)
helper.repeatOnLifecycle {
this.title = title
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_title">关于项目</string>
</resources>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_title">about project</string>
</resources>

View File

@ -30,6 +30,7 @@ import com.m3u.ui.components.TextField
import com.m3u.ui.model.LocalHelper
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.repeatOnLifecycle
import com.m3u.i18n.R as I18R
@Composable
internal fun ConsoleRoute(
@ -37,7 +38,7 @@ internal fun ConsoleRoute(
viewModel: ConsoleViewModel = hiltViewModel()
) {
val helper = LocalHelper.current
val title = stringResource(R.string.console_title)
val title = stringResource(I18R.string.feat_console_title)
helper.repeatOnLifecycle {
this.title = title
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="console_title">Console Editor</string>
</resources>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="console_title">Console Editor</string>
</resources>

View File

@ -25,7 +25,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import com.m3u.data.database.entity.Live
import com.m3u.features.favorite.R
import com.m3u.ui.components.Image
import com.m3u.ui.components.TextBadge
import com.m3u.ui.model.LocalScalable
@ -33,6 +32,7 @@ import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.LocalTheme
import com.m3u.ui.ktx.animated
import java.net.URI
import com.m3u.i18n.R as I18R
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -52,7 +52,7 @@ internal fun FavoriteItem(
val actualBackgroundColor by theme.surface.animated("FavoriteItemBackground")
val actualContentColor by theme.onSurface.animated("FavoriteItemContent")
val scheme = remember(live) {
URI(live.url).scheme ?: context.getString(R.string.scheme_unknown).uppercase()
URI(live.url).scheme ?: context.getString(I18R.string.feat_feed_scheme_unknown).uppercase()
}
Surface(
shape = RoundedCornerShape(spacing.medium),

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="scheme_unknown">未知</string>
</resources>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="scheme_unknown">unknown</string>
</resources>

View File

@ -94,6 +94,7 @@ import com.m3u.ui.model.Scalable
import com.m3u.ui.model.repeatOnLifecycle
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.m3u.i18n.R as I18R
internal typealias NavigateToLive = (liveId: Int) -> Unit
internal typealias NavigateToPlaylist = (playlist: List<Int>, initial: Int) -> Unit
@ -259,7 +260,7 @@ private fun FeedScreen(
onValueChange = onQuery,
fontWeight = FontWeight.Bold,
height = 32.dp,
placeholder = stringResource(R.string.query_placeholder).capitalize(Locale.current),
placeholder = stringResource(I18R.string.feat_feed_query_placeholder).capitalize(Locale.current),
modifier = Modifier
.padding(spacing.medium)
.fillMaxWidth()

View File

@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.m3u.i18n.R as I18R
@HiltViewModel
class FeedViewModel @Inject constructor(
@ -54,7 +55,7 @@ class FeedViewModel @Inject constructor(
private fun observe(feedUrl: String) {
observeJob?.cancel()
if (feedUrl.isEmpty()) {
val message = string(R.string.error_observe_feed, "")
val message = string(I18R.string.feat_feed_error_observe_feed, "")
onMessage(message)
return
}
@ -74,7 +75,7 @@ class FeedViewModel @Inject constructor(
)
}
} else {
val message = string(R.string.error_observe_feed, feedUrl)
val message = string(I18R.string.feat_feed_error_observe_feed, feedUrl)
onMessage(message)
}
}

View File

@ -14,11 +14,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import com.m3u.data.database.entity.Live
import com.m3u.features.feed.R
import com.m3u.ui.components.DialogTextField
import com.m3u.ui.components.DialogItem
import com.m3u.ui.components.AppDialog
import com.m3u.ui.model.LocalSpacing
import com.m3u.i18n.R as I18R
internal typealias OnUpdateDialogStatus = (DialogStatus) -> Unit
internal typealias OnFavoriteLive = (liveId: Int, target: Boolean) -> Unit
@ -48,18 +48,18 @@ internal fun FeedDialog(
)
val favourite = status.live.favourite
DialogItem(
if (favourite) R.string.dialog_favourite_cancel_title
else R.string.dialog_favourite_title
if (favourite) I18R.string.feat_feed_dialog_favourite_cancel_title
else I18R.string.feat_feed_dialog_favourite_title
) {
onUpdate(DialogStatus.Idle)
onFavorite(status.live.id, !favourite)
}
DialogItem(R.string.dialog_mute_title) {
DialogItem(I18R.string.feat_feed_dialog_mute_title) {
onUpdate(DialogStatus.Idle)
onBanned(status.live.id, true)
}
if (!status.live.cover.isNullOrEmpty()) {
DialogItem(R.string.dialog_save_picture_title) {
DialogItem(I18R.string.feat_feed_dialog_save_picture_title) {
onUpdate(DialogStatus.Idle)
onSavePicture(status.live.id)
}

View File

@ -28,13 +28,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import com.m3u.data.database.entity.Live
import com.m3u.features.feed.R
import com.m3u.ui.components.Image
import com.m3u.ui.components.TextBadge
import com.m3u.ui.model.LocalScalable
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.LocalTheme
import java.net.URI
import com.m3u.i18n.R as I18R
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -57,7 +57,7 @@ internal fun LiveItem(
URI(live.url).scheme
} catch (ignored: Exception) {
null
} ?: context.getString(R.string.scheme_unknown).uppercase()
} ?: context.getString(I18R.string.feat_feed_scheme_unknown).uppercase()
}
Surface(
shape = RoundedCornerShape(spacing.medium),

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="scheme_unknown">未知</string>
<string name="dialog_menu_title">屏蔽这个频道?</string>
<string name="dialog_favourite_title">喜欢</string>
<string name="dialog_favourite_cancel_title">取消喜欢</string>
<string name="dialog_mute_title">屏蔽</string>
<string name="error_observe_feed">订阅不存在(%s</string>
<string name="dialog_save_picture_title">保存封面</string>
<string name="query_placeholder">输入关键字</string>
</resources>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="scheme_unknown">unknown</string>
<string name="dialog_menu_title">do you want to ban this live?</string>
<string name="dialog_favourite_title">like</string>
<string name="dialog_favourite_cancel_title">cancel like</string>
<string name="dialog_mute_title">ban</string>
<string name="error_observe_feed">playlist is not existed (%s)</string>
<string name="dialog_save_picture_title">save to gallery</string>
<string name="query_placeholder">enter key word</string>
</resources>

View File

@ -24,12 +24,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.m3u.features.live.R
import com.m3u.ui.components.CircularProgressIndicator
import com.m3u.ui.components.MaskState
import com.m3u.ui.components.OnDismiss
import com.m3u.ui.model.LocalSpacing
import net.mm2d.upnp.Device
import com.m3u.i18n.R as I18R
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
@ -67,7 +67,7 @@ fun DlnaDevicesBottomSheet(
)
) {
Text(
text = stringResource(R.string.dlna_devices),
text = stringResource(I18R.string.feat_live_dlna_devices),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6,
modifier = Modifier.weight(1f)

View File

@ -39,7 +39,6 @@ import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import com.m3u.core.annotation.ClipMode
import com.m3u.core.util.basic.isNotEmpty
import com.m3u.features.live.R
import com.m3u.features.live.components.CoverPlaceholder
import com.m3u.features.live.components.LiveMask
import com.m3u.ui.components.Background
@ -52,6 +51,7 @@ import com.m3u.ui.components.rememberPlayerState
import com.m3u.ui.model.LocalHelper
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.LocalTheme
import com.m3u.i18n.R as I18R
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -114,7 +114,7 @@ internal fun LiveFragment(
state = maskState,
icon = Icons.AutoMirrored.Rounded.ArrowBack,
onClick = onBackPressed,
contentDescription = stringResource(R.string.tooltip_on_back_pressed)
contentDescription = stringResource(I18R.string.feat_live_tooltip_on_back_pressed)
)
Spacer(modifier = Modifier.weight(1f))
MaskButton(
@ -122,16 +122,16 @@ internal fun LiveFragment(
icon = if (muted) Icons.AutoMirrored.Rounded.VolumeMute
else Icons.AutoMirrored.Rounded.VolumeUp,
onClick = onMuted,
contentDescription = if (muted) stringResource(R.string.tooltip_unmute)
else stringResource(R.string.tooltip_mute)
contentDescription = if (muted) stringResource(I18R.string.feat_live_tooltip_unmute)
else stringResource(I18R.string.feat_live_tooltip_mute)
)
MaskButton(
state = maskState,
icon = Icons.Rounded.Star,
tint = if (stared) Color.Yellow else Color.Unspecified,
onClick = onFavourite,
contentDescription = if (stared) stringResource(R.string.tooltip_unfavourite)
else stringResource(R.string.tooltip_favourite)
contentDescription = if (stared) stringResource(I18R.string.feat_live_tooltip_unfavourite)
else stringResource(I18R.string.feat_live_tooltip_favourite)
)
if (experimentalMode) {
MaskButton(
@ -142,8 +142,8 @@ internal fun LiveFragment(
tint = if (recording) LocalTheme.current.error
else Color.Unspecified,
onClick = onRecord,
contentDescription = if (recording) stringResource(R.string.tooltip_unrecord)
else stringResource(R.string.tooltip_record)
contentDescription = if (recording) stringResource(I18R.string.feat_live_tooltip_unrecord)
else stringResource(I18R.string.feat_live_tooltip_record)
)
if (playback != Player.STATE_IDLE) {
MaskButton(
@ -151,7 +151,7 @@ internal fun LiveFragment(
enabled = false,
icon = Icons.Rounded.Cast,
onClick = searchDlnaDevices,
contentDescription = stringResource(R.string.tooltip_cast)
contentDescription = stringResource(I18R.string.feat_live_tooltip_cast)
)
}
}
@ -163,7 +163,7 @@ internal fun LiveFragment(
helper.enterPipMode(videoSize)
maskState.sleep()
},
contentDescription = stringResource(R.string.tooltip_enter_pip_mode)
contentDescription = stringResource(I18R.string.feat_live_tooltip_enter_pip_mode)
)
}
},
@ -255,10 +255,10 @@ private val PlaybackException?.displayText: String
private val @Player.State Int.displayText: String
@Composable get() = when (this) {
Player.STATE_IDLE -> R.string.playback_state_idle
Player.STATE_BUFFERING -> R.string.playback_state_buffering
Player.STATE_IDLE -> I18R.string.feat_live_playback_state_idle
Player.STATE_BUFFERING -> I18R.string.feat_live_playback_state_buffering
Player.STATE_READY -> null
Player.STATE_ENDED -> R.string.playback_state_ended
Player.STATE_ENDED -> I18R.string.feat_live_playback_state_ended
else -> null
}
?.let { stringResource(it) }

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="playback_state_idle">空闲</string>
<string name="playback_state_buffering">缓冲中</string>
<string name="playback_state_ready">就绪</string>
<string name="playback_state_ended">结束</string>
<string name="tooltip_on_back_pressed">返回</string>
<string name="tooltip_mute">静音</string>
<string name="tooltip_unmute">取消静音</string>
<string name="tooltip_favourite">收藏</string>
<string name="tooltip_unfavourite">取消收藏</string>
<string name="tooltip_record">录制</string>
<string name="tooltip_unrecord">停止录制</string>
<string name="tooltip_cast">投屏</string>
<string name="tooltip_enter_pip_mode">画中画模式</string>
<string name="dlna_devices">DLNA 设备</string>
</resources>

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="playback_state_idle">idle</string>
<string name="playback_state_buffering">buffering</string>
<string name="playback_state_ready">ready</string>
<string name="playback_state_ended">completed</string>
<string name="tooltip_on_back_pressed">back</string>
<string name="tooltip_mute">mute</string>
<string name="tooltip_unmute">unmute</string>
<string name="tooltip_favourite">favourite</string>
<string name="tooltip_unfavourite">unfavourite</string>
<string name="tooltip_record">record</string>
<string name="tooltip_unrecord">unrecord</string>
<string name="tooltip_cast">cast screen</string>
<string name="tooltip_enter_pip_mode">PIP mode</string>
<string name="dlna_devices">DLNA Devices</string>
</resources>

View File

@ -36,6 +36,7 @@ import com.m3u.ui.model.LocalHelper
import com.m3u.ui.model.LocalScalable
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.Scalable
import com.m3u.i18n.R as I18R
private typealias showDialog = (Feed) -> Unit
typealias NavigateToFeed = (feed: Feed) -> Unit
@ -213,7 +214,7 @@ private fun LandscapeOrientationContent(
@Composable
private fun Feed.calculateUiTitle(): AnnotatedString {
val actual = title.ifEmpty {
if (local) stringResource(R.string.imported_feed_title)
if (local) stringResource(I18R.string.feat_main_imported_feed_title)
else ""
}
return AnnotatedString(

View File

@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.m3u.i18n.R as I18R
@HiltViewModel
class MainViewModel @Inject constructor(
@ -70,7 +71,7 @@ class MainViewModel @Inject constructor(
viewModelScope.launch {
val feed = feedRepository.unsubscribe(url)
if (feed == null) {
val message = string(R.string.error_unsubscribe_feed)
val message = string(I18R.string.feat_main_error_unsubscribe_feed)
logger.log(message)
}
}

View File

@ -15,13 +15,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.m3u.data.database.entity.Feed
import com.m3u.features.main.R
import com.m3u.ui.components.AppDialog
import com.m3u.ui.components.DialogItem
import com.m3u.ui.components.DialogTextField
import com.m3u.ui.ktx.animateDp
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.LocalTheme
import com.m3u.i18n.R as I18R
internal typealias OnUpdateStatus = (MainDialog) -> Unit
internal typealias OnUnsubscribe = (feedUrl: String) -> Unit
@ -68,7 +68,7 @@ internal fun MainDialog(
var renamedText by remember(currentStatus) {
mutableStateOf(
with(currentStatus.feed) {
if (editable) title else context.getString(R.string.imported_feed_title)
if (editable) title else context.getString(I18R.string.feat_main_imported_feed_title)
}
)
}
@ -87,13 +87,13 @@ internal fun MainDialog(
}
)
if (!editMode) {
DialogItem(R.string.unsubscribe_feed) {
DialogItem(I18R.string.feat_main_unsubscribe_feed) {
unsubscribe(currentStatus.feed.url)
update(MainDialog.Idle)
}
if (!currentStatus.feed.local) {
val clipboardManager = LocalClipboardManager.current
DialogItem(R.string.copy_feed_url) {
DialogItem(I18R.string.feat_main_copy_feed_url) {
val annotatedString = AnnotatedString(currentStatus.feed.url)
clipboardManager.setText(annotatedString)
update(MainDialog.Idle)

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="muted_lives_feed">屏蔽的频道</string>
<string name="unsubscribe_feed">取消订阅</string>
<string name="error_unsubscribe_feed">取消订阅失败</string>
<string name="copy_feed_url">复制链接</string>
<string name="rename_feed">重命名</string>
<string name="imported_feed_title">导入频道</string>
</resources>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="muted_lives_feed">banned lives</string>
<string name="unsubscribe_feed">unsubscribe</string>
<string name="error_unsubscribe_feed">cannot unsubscribe</string>
<string name="copy_feed_url">copy url</string>
<string name="rename_feed">rename</string>
<string name="imported_feed_title">imported</string>
</resources>

View File

@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.m3u.i18n.R as I18R
@HiltViewModel
class SettingViewModel @Inject constructor(
@ -159,7 +160,7 @@ class SettingViewModel @Inject constructor(
private fun subscribe() {
val title = writable.value.title
if (title.isEmpty()) {
val message = string(R.string.error_empty_title)
val message = string(I18R.string.feat_setting_error_empty_title)
logger.log(message)
return
}
@ -167,8 +168,8 @@ class SettingViewModel @Inject constructor(
val url = readable.actualUrl
if (url == null) {
val message = when {
readable.localStorage -> string(R.string.error_unselected_file)
else -> string(R.string.error_blank_url)
readable.localStorage -> string(I18R.string.feat_setting_error_unselected_file)
else -> string(I18R.string.feat_setting_error_blank_url)
}
logger.log(message)
return
@ -186,7 +187,7 @@ class SettingViewModel @Inject constructor(
.addTag(url)
.build()
workManager.enqueue(request)
val message = string(R.string.enqueue_subscribe)
val message = string(I18R.string.feat_setting_enqueue_subscribe)
logger.log(message)
writable.update {
it.copy(

View File

@ -27,13 +27,13 @@ import com.m3u.core.annotation.OnClipMode
import com.m3u.core.annotation.OnFeedStrategy
import com.m3u.features.setting.NavigateToAbout
import com.m3u.features.setting.NavigateToConsole
import com.m3u.features.setting.R
import com.m3u.features.setting.components.CheckBoxPreference
import com.m3u.features.setting.components.IconPreference
import com.m3u.features.setting.components.Preference
import com.m3u.features.setting.components.TextPreference
import com.m3u.ui.Destination
import com.m3u.ui.model.LocalSpacing
import com.m3u.i18n.R as I18R
@Composable
internal fun PreferencesFragment(
@ -84,43 +84,43 @@ internal fun PreferencesFragment(
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
Preference(
title = stringResource(R.string.feed_management),
title = stringResource(I18R.string.feat_setting_feed_management),
enabled = true,
onClick = onFeedManagement
)
TextPreference(
title = stringResource(R.string.sync_mode),
title = stringResource(I18R.string.feat_setting_sync_mode),
content = when (feedStrategy) {
FeedStrategy.ALL -> stringResource(R.string.sync_mode_all)
FeedStrategy.SKIP_FAVORITE -> stringResource(R.string.sync_mode_skip_favourite)
FeedStrategy.ALL -> stringResource(I18R.string.feat_setting_sync_mode_all)
FeedStrategy.SKIP_FAVORITE -> stringResource(I18R.string.feat_setting_sync_mode_skip_favourite)
else -> ""
},
onClick = onFeedStrategy
)
TextPreference(
title = stringResource(R.string.clip_mode),
title = stringResource(I18R.string.feat_setting_clip_mode),
content = when (clipMode) {
ClipMode.ADAPTIVE -> stringResource(R.string.clip_mode_adaptive)
ClipMode.CLIP -> stringResource(R.string.clip_mode_clip)
ClipMode.STRETCHED -> stringResource(R.string.clip_mode_stretched)
ClipMode.ADAPTIVE -> stringResource(I18R.string.feat_setting_clip_mode_adaptive)
ClipMode.CLIP -> stringResource(I18R.string.feat_setting_clip_mode_clip)
ClipMode.STRETCHED -> stringResource(I18R.string.feat_setting_clip_mode_stretched)
else -> ""
},
onClick = onClipMode
)
TextPreference(
title = stringResource(R.string.connect_timeout),
title = stringResource(I18R.string.feat_setting_connect_timeout),
content = "${connectTimeout / 1000}s",
onClick = onConnectTimeout
)
TextPreference(
title = stringResource(R.string.initial_tab),
title = stringResource(I18R.string.feat_setting_initial_tab),
content = stringResource(Destination.Root.entries[initialRootDestination].iconTextId),
onClick = onInitialTabIndex
)
CheckBoxPreference(
title = stringResource(R.string.auto_refresh),
subtitle = stringResource(R.string.auto_refresh_description),
title = stringResource(I18R.string.feat_setting_auto_refresh),
subtitle = stringResource(I18R.string.feat_setting_auto_refresh_description),
checked = autoRefresh,
onCheckedChange = { newValue ->
if (newValue != autoRefresh) {
@ -129,8 +129,8 @@ internal fun PreferencesFragment(
}
)
CheckBoxPreference(
title = stringResource(R.string.no_picture_mode),
subtitle = stringResource(R.string.no_picture_mode_description),
title = stringResource(I18R.string.feat_setting_no_picture_mode),
subtitle = stringResource(I18R.string.feat_setting_no_picture_mode_description),
checked = noPictureMode,
onCheckedChange = { newValue ->
if (newValue != noPictureMode) {
@ -139,8 +139,8 @@ internal fun PreferencesFragment(
}
)
CheckBoxPreference(
title = stringResource(R.string.full_info_player),
subtitle = stringResource(R.string.full_info_player_description),
title = stringResource(I18R.string.feat_setting_full_info_player),
subtitle = stringResource(I18R.string.feat_setting_full_info_player_description),
checked = fullInfoPlayer,
onCheckedChange = { newValue ->
if (newValue != fullInfoPlayer) {
@ -149,8 +149,8 @@ internal fun PreferencesFragment(
}
)
CheckBoxPreference(
title = stringResource(R.string.god_mode),
subtitle = stringResource(R.string.god_mode_description),
title = stringResource(I18R.string.feat_setting_god_mode),
subtitle = stringResource(I18R.string.feat_setting_god_mode_description),
checked = godMode,
onCheckedChange = { newValue ->
if (newValue != godMode) {
@ -159,9 +159,9 @@ internal fun PreferencesFragment(
}
)
CheckBoxPreference(
title = stringResource(R.string.common_ui_mode),
subtitle = if (useCommonUIModeEnable) stringResource(R.string.common_ui_mode_description)
else stringResource(R.string.common_ui_mode_disabled_description),
title = stringResource(I18R.string.feat_setting_common_ui_mode),
subtitle = if (useCommonUIModeEnable) stringResource(I18R.string.feat_setting_common_ui_mode_description)
else stringResource(I18R.string.feat_setting_common_ui_mode_disabled_description),
enabled = useCommonUIModeEnable,
checked = useCommonUIMode,
onCheckedChange = { newValue ->
@ -181,8 +181,8 @@ internal fun PreferencesFragment(
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
CheckBoxPreference(
title = stringResource(R.string.experimental_mode),
subtitle = stringResource(R.string.experimental_mode_description),
title = stringResource(I18R.string.feat_setting_experimental_mode),
subtitle = stringResource(I18R.string.feat_setting_experimental_mode_description),
checked = experimentalMode,
onCheckedChange = { newValue ->
if (newValue != experimentalMode) {
@ -203,8 +203,8 @@ internal fun PreferencesFragment(
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
CheckBoxPreference(
title = stringResource(R.string.cinema_mode),
subtitle = stringResource(R.string.cinema_mode_description),
title = stringResource(I18R.string.feat_setting_cinema_mode),
subtitle = stringResource(I18R.string.feat_setting_cinema_mode_description),
checked = cinemaMode,
onCheckedChange = { newValue ->
if (newValue != cinemaMode) {
@ -213,16 +213,16 @@ internal fun PreferencesFragment(
}
)
Preference(
title = stringResource(R.string.script_management),
title = stringResource(I18R.string.feat_setting_script_management),
enabled = true,
onClick = onScriptManagement
)
Preference(
title = stringResource(R.string.console_editor),
title = stringResource(I18R.string.feat_setting_console_editor),
onClick = navigateToConsole
)
CheckBoxPreference(
title = stringResource(R.string.scroll_mode),
title = stringResource(I18R.string.feat_setting_scroll_mode),
checked = scrollMode,
onCheckedChange = { newValue ->
if (newValue != scrollMode) {
@ -231,8 +231,8 @@ internal fun PreferencesFragment(
}
)
CheckBoxPreference(
title = stringResource(R.string.ssl_verification_enabled),
subtitle = stringResource(R.string.ssl_verification_enabled_description),
title = stringResource(I18R.string.feat_setting_ssl_verification_enabled),
subtitle = stringResource(I18R.string.feat_setting_ssl_verification_enabled_description),
checked = isSSLVerificationEnabled,
onCheckedChange = { newValue ->
if (newValue != isSSLVerificationEnabled) {
@ -253,7 +253,7 @@ internal fun PreferencesFragment(
) {
val context = LocalContext.current
IconPreference(
title = stringResource(R.string.system_setting),
title = stringResource(I18R.string.feat_setting_system_setting),
imageVector = Icons.AutoMirrored.Rounded.OpenInNew,
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@ -264,17 +264,17 @@ internal fun PreferencesFragment(
)
// TODO: https://www.dropbox.com/developers/documentation/http/documentation#file_requests-list
Preference(
title = stringResource(R.string.dropbox).uppercase(),
title = stringResource(I18R.string.feat_setting_dropbox).uppercase(),
onClick = navigateToAbout,
enabled = false
)
Preference(
title = stringResource(R.string.project_about),
title = stringResource(I18R.string.feat_setting_project_about),
onClick = navigateToAbout,
// enabled = false
)
Preference(
title = stringResource(R.string.app_version),
title = stringResource(I18R.string.feat_setting_app_version),
subtitle = version
)
}

View File

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.m3u.features.setting.R
import com.m3u.ui.components.Button
import com.m3u.ui.components.OuterColumn
import com.m3u.i18n.R as I18R
@Composable
internal fun ScriptsFragment(
@ -33,7 +33,7 @@ internal fun ScriptsFragment(
}
Button(
textRes = R.string.script_management_import_js,
textRes = I18R.string.feat_setting_script_management_import_js,
modifier = Modifier.fillMaxWidth(),
onClick = {
launcher.launch(arrayOf("text/javascript"))

View File

@ -39,13 +39,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.m3u.core.util.readContentFilename
import com.m3u.data.database.entity.Live
import com.m3u.features.setting.R
import com.m3u.features.setting.components.MutedLiveItem
import com.m3u.ui.components.Button
import com.m3u.ui.components.LabelField
import com.m3u.ui.components.TextButton
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.LocalTheme
import com.m3u.i18n.R as I18R
@Composable
internal fun SubscriptionsFragment(
@ -80,7 +80,7 @@ internal fun SubscriptionsFragment(
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
Text(
text = stringResource(R.string.label_muted_lives),
text = stringResource(I18R.string.feat_setting_label_muted_lives),
style = MaterialTheme.typography.button,
color = theme.onTint,
modifier = Modifier
@ -105,7 +105,7 @@ internal fun SubscriptionsFragment(
item {
LabelField(
text = title,
placeholder = stringResource(R.string.placeholder_title).uppercase(),
placeholder = stringResource(I18R.string.feat_setting_placeholder_title).uppercase(),
onValueChange = onTitle,
keyboardActions = KeyboardActions(
onNext = {
@ -124,7 +124,7 @@ internal fun SubscriptionsFragment(
if (!localStorage) {
LabelField(
text = url,
placeholder = stringResource(R.string.placeholder_url).uppercase(),
placeholder = stringResource(I18R.string.feat_setting_placeholder_url).uppercase(),
onValueChange = onUrl,
keyboardActions = KeyboardActions(
onDone = {
@ -154,7 +154,7 @@ internal fun SubscriptionsFragment(
item {
Column {
Button(
text = stringResource(R.string.label_subscribe),
text = stringResource(I18R.string.feat_setting_label_subscribe),
onClick = onSubscribe,
modifier = Modifier.fillMaxWidth()
)
@ -189,7 +189,7 @@ fun LocalStorageSwitch(
horizontalArrangement = Arrangement.spacedBy(spacing.medium)
) {
Text(
text = stringResource(R.string.local_storage),
text = stringResource(I18R.string.feat_setting_local_storage),
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.weight(1f)
)
@ -214,7 +214,7 @@ private fun LocalStorageButton(
val icon = Icons.AutoMirrored.Rounded.OpenInNew
val text = if (selected) remember(uri) {
uri?.readContentFilename(context.contentResolver).orEmpty()
} else stringResource(R.string.label_select_from_local_storage)
} else stringResource(I18R.string.feat_setting_label_select_from_local_storage)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@ -262,7 +262,7 @@ private fun ClipboardButton(
val clipboardManager = LocalClipboardManager.current
TextButton(
enabled = enabled,
text = stringResource(R.string.label_parse_from_clipboard),
text = stringResource(I18R.string.feat_setting_label_parse_from_clipboard),
onClick = {
val clipboardUrl = clipboardManager.getText()?.text.orEmpty()
val clipboardTitle = run {

View File

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="placeholder_url">订阅链接</string>
<string name="app_version">应用版本</string>
<string name="label_subscribe">订阅</string>
<string name="success_subscribe">订阅成功</string>
<string name="failed_malformed_url">URL错误%s</string>
<string name="error_empty_title">订阅名称为空</string>
<string name="placeholder_title">订阅名称</string>
<string name="label_subscribing">订阅中…</string>
<string name="sync_mode_all">所有</string>
<string name="sync_mode_skip_favourite">绕过收藏夹</string>
<string name="sync_mode">同步策略</string>
<string name="common_ui_mode">默认布局</string>
<string name="common_ui_mode_description">使用手机设备布局模式</string>
<string name="common_ui_mode_disabled_description">手机设备不可调整</string>
<string name="script_management">脚本管理</string>
<string name="feed_management">订阅管理</string>
<string name="connect_timeout">最大超时时间</string>
<string name="god_mode">上帝模式</string>
<string name="god_mode_description">通过物理按键调节界面</string>
<string name="console_editor">console editor</string>
<string name="experimental_mode">试验特性</string>
<string name="experimental_mode_description">不稳定的功能可能引发致命错误</string>
<string name="label_muted_lives">屏蔽的频道</string>
<string name="label_add_feed">新增订阅</string>
<string name="label_parse_from_clipboard">解析剪贴板</string>
<string name="label_select_from_local_storage">选择文件</string>
<string name="label_parse_from_disk">解析本地文件</string>
<string name="clip_mode">视频裁剪模式</string>
<string name="clip_mode_adaptive">自适应</string>
<string name="clip_mode_clip">裁剪</string>
<string name="clip_mode_stretched">拉伸</string>
<string name="scroll_mode">滑动切换频道</string>
<string name="auto_refresh">自动刷新</string>
<string name="auto_refresh_description">进入播放列表自动刷新</string>
<string name="ssl_verification_enabled">SSL证书校验</string>
<string name="ssl_verification_enabled_description">校验网络连接安全性</string>
<string name="full_info_player">详细信息的播放器</string>
<string name="full_info_player_description">播放器中展示更多的信息</string>
<string name="initial_tab">初始界面</string>
<string name="no_picture_mode">无图模式</string>
<string name="no_picture_mode_description">可能会对性能有所提升</string>
<string name="system_setting">系统设置</string>
<string name="cinema_mode">影院模式</string>
<string name="cinema_mode_description">使用OLED纯黑主题</string>
<string name="script_management_import_js">导入Javascript</string>
<string name="enqueue_subscribe">订阅任务已添加至队列</string>
<string name="dropbox">dropbox</string>
<string name="project_about">关于项目</string>
<string name="local_storage">本地导入</string>
<string name="error_unselected_file">还未选择文件</string>
<string name="error_blank_url">目标地址为空</string>
</resources>

View File

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="placeholder_url">playlist link</string>
<string name="app_version">app version</string>
<string name="label_subscribe">subscribe</string>
<string name="success_subscribe">subscribe successfully</string>
<string name="failed_malformed_url">malformed url (%s)</string>
<string name="error_empty_title">playlist name is empty</string>
<string name="placeholder_title">playlist name</string>
<string name="label_subscribing">subscribing</string>
<string name="sync_mode_all">all</string>
<string name="sync_mode_skip_favourite">skip favourite</string>
<string name="sync_mode">synchronization strategy</string>
<string name="common_ui_mode">UI mode</string>
<string name="common_ui_mode_description">use mobile device UI mode</string>
<string name="common_ui_mode_disabled_description">mobile device could not adjust this</string>
<string name="script_management">script management</string>
<string name="feed_management">playlist management</string>
<string name="connect_timeout">connect timeout</string>
<string name="god_mode">god mode</string>
<string name="god_mode_description">adjust layouts by physical volume buttons</string>
<string name="console_editor">Console Editor</string>
<string name="experimental_mode">experimental mode</string>
<string name="experimental_mode_description">unstable features can throw fatal errors</string>
<string name="label_muted_lives">banned lives</string>
<string name="label_add_feed">add playlist</string>
<string name="label_parse_from_clipboard">parse clipboard</string>
<string name="label_select_from_local_storage">select file</string>
<string name="label_parse_from_disk">parse file</string>
<string name="clip_mode">video clip mode</string>
<string name="clip_mode_adaptive">adaptive</string>
<string name="clip_mode_clip">clip</string>
<string name="clip_mode_stretched">stretched</string>
<string name="scroll_mode">scroll to change lives</string>
<string name="auto_refresh">auto refresh</string>
<string name="auto_refresh_description">automatically refreshed when enter the playlist</string>
<string name="ssl_verification_enabled">SSL certificate verification</string>
<string name="ssl_verification_enabled_description">ensure valid and trustworthy SSL/TLS certificates for secure communications</string>
<string name="full_info_player">full information player</string>
<string name="full_info_player_description">display more information in the player</string>
<string name="initial_tab">initial Screen</string>
<string name="no_picture_mode">no picture mode</string>
<string name="no_picture_mode_description">may improve performance</string>
<string name="system_setting">system setting</string>
<string name="cinema_mode">cinema mode</string>
<string name="cinema_mode_description">use OLED black theme</string>
<string name="script_management_import_js">import Javascript</string>
<string name="enqueue_subscribe">a subscription task has been added to the queue</string>
<string name="dropbox">dropbox</string>
<string name="project_about">about project</string>
<string name="local_storage">local storage</string>
<string name="error_unselected_file">no file selected yet</string>
<string name="error_blank_url">blank url</string>
</resources>

1
i18n/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

38
i18n/build.gradle.kts Normal file
View File

@ -0,0 +1,38 @@
plugins {
alias(libs.plugins.com.android.library)
alias(libs.plugins.org.jetbrains.kotlin.android)
}
android {
namespace = "com.m3u.i18n"
compileSdk = 33
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.core.ktx)
implementation(libs.androidx.appcompat.appcompat)
}

0
i18n/consumer-rules.pro Normal file
View File

21
i18n/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label_crash">应用发生崩溃</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="data_error_file_not_found">文件不存在</string>
<string name="data_error_empty_title">订阅名称为空</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_about_title">关于项目</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_console_title">Console Editor</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_favorite_scheme_unknown">未知</string>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_feed_scheme_unknown">未知</string>
<string name="feat_feed_dialog_menu_title">屏蔽这个频道?</string>
<string name="feat_feed_dialog_favourite_title">喜欢</string>
<string name="feat_feed_dialog_favourite_cancel_title">取消喜欢</string>
<string name="feat_feed_dialog_mute_title">屏蔽</string>
<string name="feat_feed_error_observe_feed">订阅不存在(%s</string>
<string name="feat_feed_dialog_save_picture_title">保存封面</string>
<string name="feat_feed_query_placeholder">输入关键字</string>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_live_playback_state_idle">空闲</string>
<string name="feat_live_playback_state_buffering">缓冲中</string>
<string name="feat_live_playback_state_ready">就绪</string>
<string name="feat_live_playback_state_ended">结束</string>
<string name="feat_live_tooltip_on_back_pressed">返回</string>
<string name="feat_live_tooltip_mute">静音</string>
<string name="feat_live_tooltip_unmute">取消静音</string>
<string name="feat_live_tooltip_favourite">收藏</string>
<string name="feat_live_tooltip_unfavourite">取消收藏</string>
<string name="feat_live_tooltip_record">录制</string>
<string name="feat_live_tooltip_unrecord">停止录制</string>
<string name="feat_live_tooltip_cast">投屏</string>
<string name="feat_live_tooltip_enter_pip_mode">画中画模式</string>
<string name="feat_live_dlna_devices">DLNA 设备</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_main_muted_lives_feed">屏蔽的频道</string>
<string name="feat_main_unsubscribe_feed">取消订阅</string>
<string name="feat_main_error_unsubscribe_feed">取消订阅失败</string>
<string name="feat_main_copy_feed_url">复制链接</string>
<string name="feat_main_rename_feed">重命名</string>
<string name="feat_main_imported_feed_title">导入频道</string>
</resources>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_setting_placeholder_url">订阅链接</string>
<string name="feat_setting_app_version">应用版本</string>
<string name="feat_setting_label_subscribe">订阅</string>
<string name="feat_setting_success_subscribe">订阅成功</string>
<string name="feat_setting_failed_malformed_url">URL错误%s</string>
<string name="feat_setting_error_empty_title">订阅名称为空</string>
<string name="feat_setting_placeholder_title">订阅名称</string>
<string name="feat_setting_label_subscribing">订阅中…</string>
<string name="feat_setting_sync_mode_all">所有</string>
<string name="feat_setting_sync_mode_skip_favourite">绕过收藏夹</string>
<string name="feat_setting_sync_mode">同步策略</string>
<string name="feat_setting_common_ui_mode">默认布局</string>
<string name="feat_setting_common_ui_mode_description">使用手机设备布局模式</string>
<string name="feat_setting_common_ui_mode_disabled_description">手机设备不可调整</string>
<string name="feat_setting_script_management">脚本管理</string>
<string name="feat_setting_feed_management">订阅管理</string>
<string name="feat_setting_connect_timeout">最大超时时间</string>
<string name="feat_setting_god_mode">上帝模式</string>
<string name="feat_setting_god_mode_description">通过物理按键调节界面</string>
<string name="feat_setting_console_editor">console editor</string>
<string name="feat_setting_experimental_mode">试验特性</string>
<string name="feat_setting_experimental_mode_description">不稳定的功能可能引发致命错误</string>
<string name="feat_setting_label_muted_lives">屏蔽的频道</string>
<string name="feat_setting_label_add_feed">新增订阅</string>
<string name="feat_setting_label_parse_from_clipboard">解析剪贴板</string>
<string name="feat_setting_label_select_from_local_storage">选择文件</string>
<string name="feat_setting_label_parse_from_disk">解析本地文件</string>
<string name="feat_setting_clip_mode">视频裁剪模式</string>
<string name="feat_setting_clip_mode_adaptive">自适应</string>
<string name="feat_setting_clip_mode_clip">裁剪</string>
<string name="feat_setting_clip_mode_stretched">拉伸</string>
<string name="feat_setting_scroll_mode">滑动切换频道</string>
<string name="feat_setting_auto_refresh">自动刷新</string>
<string name="feat_setting_auto_refresh_description">进入播放列表自动刷新</string>
<string name="feat_setting_ssl_verification_enabled">SSL证书校验</string>
<string name="feat_setting_ssl_verification_enabled_description">校验网络连接安全性</string>
<string name="feat_setting_full_info_player">详细信息的播放器</string>
<string name="feat_setting_full_info_player_description">播放器中展示更多的信息</string>
<string name="feat_setting_initial_tab">初始界面</string>
<string name="feat_setting_no_picture_mode">无图模式</string>
<string name="feat_setting_no_picture_mode_description">可能会对性能有所提升</string>
<string name="feat_setting_system_setting">系统设置</string>
<string name="feat_setting_cinema_mode">影院模式</string>
<string name="feat_setting_cinema_mode_description">使用OLED纯黑主题</string>
<string name="feat_setting_script_management_import_js">导入Javascript</string>
<string name="feat_setting_enqueue_subscribe">订阅任务已添加至队列</string>
<string name="feat_setting_dropbox">dropbox</string>
<string name="feat_setting_project_about">关于项目</string>
<string name="feat_setting_local_storage">本地导入</string>
<string name="feat_setting_error_unselected_file">还未选择文件</string>
<string name="feat_setting_error_blank_url">目标地址为空</string>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ui_app_name">M3U</string>
<string name="ui_destination_main">主页</string>
<string name="ui_destination_favourite">喜欢</string>
<string name="ui_destination_setting">设置</string>
<string name="ui_title_setting">设置</string>
<string name="ui_title_favourite">喜欢</string>
<string name="ui_error_unknown">未知错误</string>
<string name="ui_cd_top_bar_on_back_pressed">返回</string>
<string name="ui_theme_card_left"></string>
<string name="ui_theme_card_right"></string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label_crash">app crashes</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="data_error_file_not_found">file not found</string>
<string name="data_error_empty_title">playlist name is empty</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_about_title">about project</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_console_title">Console Editor</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_favorite_scheme_unknown">unknown</string>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_feed_scheme_unknown">unknown</string>
<string name="feat_feed_dialog_menu_title">do you want to ban this live?</string>
<string name="feat_feed_dialog_favourite_title">like</string>
<string name="feat_feed_dialog_favourite_cancel_title">cancel like</string>
<string name="feat_feed_dialog_mute_title">ban</string>
<string name="feat_feed_error_observe_feed">playlist is not existed (%s)</string>
<string name="feat_feed_dialog_save_picture_title">save to gallery</string>
<string name="feat_feed_query_placeholder">enter key word</string>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_live_playback_state_idle">idle</string>
<string name="feat_live_playback_state_buffering">buffering</string>
<string name="feat_live_playback_state_ready">ready</string>
<string name="feat_live_playback_state_ended">completed</string>
<string name="feat_live_tooltip_on_back_pressed">back</string>
<string name="feat_live_tooltip_mute">mute</string>
<string name="feat_live_tooltip_unmute">unmute</string>
<string name="feat_live_tooltip_favourite">favourite</string>
<string name="feat_live_tooltip_unfavourite">unfavourite</string>
<string name="feat_live_tooltip_record">record</string>
<string name="feat_live_tooltip_unrecord">unrecord</string>
<string name="feat_live_tooltip_cast">cast screen</string>
<string name="feat_live_tooltip_enter_pip_mode">PIP mode</string>
<string name="feat_live_dlna_devices">DLNA Devices</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_main_muted_lives_feed">banned lives</string>
<string name="feat_main_unsubscribe_feed">unsubscribe</string>
<string name="feat_main_error_unsubscribe_feed">cannot unsubscribe</string>
<string name="feat_main_copy_feed_url">copy url</string>
<string name="feat_main_rename_feed">rename</string>
<string name="feat_main_imported_feed_title">imported</string>
</resources>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feat_setting_placeholder_url">playlist link</string>
<string name="feat_setting_app_version">app version</string>
<string name="feat_setting_label_subscribe">subscribe</string>
<string name="feat_setting_success_subscribe">subscribe successfully</string>
<string name="feat_setting_failed_malformed_url">malformed url (%s)</string>
<string name="feat_setting_error_empty_title">playlist name is empty</string>
<string name="feat_setting_placeholder_title">playlist name</string>
<string name="feat_setting_label_subscribing">subscribing</string>
<string name="feat_setting_sync_mode_all">all</string>
<string name="feat_setting_sync_mode_skip_favourite">skip favourite</string>
<string name="feat_setting_sync_mode">synchronization strategy</string>
<string name="feat_setting_common_ui_mode">UI mode</string>
<string name="feat_setting_common_ui_mode_description">use mobile device UI mode</string>
<string name="feat_setting_common_ui_mode_disabled_description">mobile device could not adjust this</string>
<string name="feat_setting_script_management">script management</string>
<string name="feat_setting_feed_management">playlist management</string>
<string name="feat_setting_connect_timeout">connect timeout</string>
<string name="feat_setting_god_mode">god mode</string>
<string name="feat_setting_god_mode_description">adjust layouts by physical volume buttons</string>
<string name="feat_setting_console_editor">Console Editor</string>
<string name="feat_setting_experimental_mode">experimental mode</string>
<string name="feat_setting_experimental_mode_description">unstable features can throw fatal errors</string>
<string name="feat_setting_label_muted_lives">banned lives</string>
<string name="feat_setting_label_add_feed">add playlist</string>
<string name="feat_setting_label_parse_from_clipboard">parse clipboard</string>
<string name="feat_setting_label_select_from_local_storage">select file</string>
<string name="feat_setting_label_parse_from_disk">parse file</string>
<string name="feat_setting_clip_mode">video clip mode</string>
<string name="feat_setting_clip_mode_adaptive">adaptive</string>
<string name="feat_setting_clip_mode_clip">clip</string>
<string name="feat_setting_clip_mode_stretched">stretched</string>
<string name="feat_setting_scroll_mode">scroll to change lives</string>
<string name="feat_setting_auto_refresh">auto refresh</string>
<string name="feat_setting_auto_refresh_description">automatically refreshed when enter the playlist</string>
<string name="feat_setting_ssl_verification_enabled">SSL certificate verification</string>
<string name="feat_setting_ssl_verification_enabled_description">ensure valid and trustworthy SSL/TLS certificates for secure communications</string>
<string name="feat_setting_full_info_player">full information player</string>
<string name="feat_setting_full_info_player_description">display more information in the player</string>
<string name="feat_setting_initial_tab">initial Screen</string>
<string name="feat_setting_no_picture_mode">no picture mode</string>
<string name="feat_setting_no_picture_mode_description">may improve performance</string>
<string name="feat_setting_system_setting">system setting</string>
<string name="feat_setting_cinema_mode">cinema mode</string>
<string name="feat_setting_cinema_mode_description">use OLED black theme</string>
<string name="feat_setting_script_management_import_js">import Javascript</string>
<string name="feat_setting_enqueue_subscribe">a subscription task has been added to the queue</string>
<string name="feat_setting_dropbox">dropbox</string>
<string name="feat_setting_project_about">about project</string>
<string name="feat_setting_local_storage">local storage</string>
<string name="feat_setting_error_unselected_file">no file selected yet</string>
<string name="feat_setting_error_blank_url">blank url</string>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ui_app_name">M3U</string>
<string name="ui_destination_main">For You</string>
<string name="ui_destination_favourite">Favourite</string>
<string name="ui_destination_setting">Settings</string>
<string name="ui_title_setting">Settings</string>
<string name="ui_title_favourite">Favourite</string>
<string name="ui_error_unknown">unknown error</string>
<string name="ui_cd_top_bar_on_back_pressed">back</string>
<string name="ui_theme_card_left">ho</string>
<string name="ui_theme_card_right">la</string>
</resources>

View File

@ -5,5 +5,5 @@ plugins {
}
dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compileOnly("com.android.tools.lint:lint:31.1.2")
compileOnly(libs.com.android.tools.lint.lint.api)
}

View File

@ -32,3 +32,4 @@ include(
)
include(":benchmark")
include(":lint")
include(":i18n")

View File

@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.Collections
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.ui.graphics.vector.ImageVector
import com.m3u.i18n.R as I18R
typealias Navigate = (Destination) -> Unit
@ -22,20 +23,20 @@ sealed interface Destination {
Main(
selectedIcon = Icons.Rounded.Home,
unselectedIcon = Icons.Outlined.Home,
iconTextId = R.string.destination_main,
titleTextId = R.string.app_name
iconTextId = I18R.string.ui_destination_main,
titleTextId = I18R.string.ui_app_name
),
Favourite(
selectedIcon = Icons.Rounded.Collections,
unselectedIcon = Icons.Outlined.Collections,
iconTextId = R.string.destination_favourite,
titleTextId = R.string.title_favourite
iconTextId = I18R.string.ui_destination_favourite,
titleTextId = I18R.string.ui_title_favourite
),
Setting(
selectedIcon = Icons.Rounded.Settings,
unselectedIcon = Icons.Outlined.Settings,
iconTextId = R.string.destination_setting,
titleTextId = R.string.title_setting
iconTextId = I18R.string.ui_destination_setting,
titleTextId = I18R.string.ui_title_setting
);
companion object : Key<Root>

View File

@ -57,7 +57,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.lerp
import com.m3u.ui.R
import com.m3u.i18n.R as I18R
import com.m3u.ui.ktx.animated
import com.m3u.ui.model.LocalDuration
import com.m3u.ui.model.LocalSpacing
@ -183,7 +183,7 @@ fun AppTopBar(
) {
IconButton(
icon = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(R.string.cd_top_bar_on_back_pressed),
contentDescription = stringResource(I18R.string.ui_cd_top_bar_on_back_pressed),
onClick = { if (progress > 0) onBackPressed?.invoke() },
modifier = Modifier.wrapContentSize()
)

View File

@ -41,7 +41,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.m3u.ui.R
import com.m3u.i18n.R as I18R
import com.m3u.ui.model.LocalSpacing
import com.m3u.ui.model.LocalTheme
import com.m3u.ui.model.SugarColors
@ -236,8 +236,8 @@ private fun ColorPiece(
modifier = Modifier.fillMaxSize()
) {
Text(
text = if (left) stringResource(R.string.theme_card_left)
else stringResource(R.string.theme_card_right),
text = if (left) stringResource(I18R.string.ui_theme_card_left)
else stringResource(I18R.string.ui_theme_card_right),
style = MaterialTheme.typography.bodyLarge
.copy(
fontSize = if (left) 16.sp

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">M3U</string>
<string name="destination_main">主页</string>
<string name="destination_favourite">喜欢</string>
<string name="destination_setting">设置</string>
<string name="title_setting">设置</string>
<string name="title_favourite">喜欢</string>
<string name="error_unknown">未知错误</string>
<string name="cd_top_bar_on_back_pressed">返回</string>
<string name="theme_card_left"></string>
<string name="theme_card_right"></string>
</resources>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">M3U</string>
<string name="destination_main">For You</string>
<string name="destination_favourite">Favourite</string>
<string name="destination_setting">Settings</string>
<string name="title_setting">Settings</string>
<string name="title_favourite">Favourite</string>
<string name="error_unknown">unknown error</string>
<string name="cd_top_bar_on_back_pressed">back</string>
<string name="theme_card_left">ho</string>
<string name="theme_card_right">la</string>
</resources>