├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── com
│ │ └── axiel7
│ │ └── tachisync
│ │ ├── App.kt
│ │ ├── data
│ │ ├── datastore
│ │ │ ├── PreferencesDataStore.kt
│ │ │ └── PreferencesRepository.kt
│ │ └── model
│ │ │ └── Manga.kt
│ │ ├── ui
│ │ ├── base
│ │ │ ├── BaseViewModel.kt
│ │ │ ├── UiEvent.kt
│ │ │ └── UiState.kt
│ │ ├── composables
│ │ │ └── MessageDialog.kt
│ │ ├── external
│ │ │ ├── ExternalEvent.kt
│ │ │ ├── ExternalUiState.kt
│ │ │ ├── ExternalView.kt
│ │ │ ├── ExternalViewModel.kt
│ │ │ └── composables
│ │ │ │ └── ExternalDeviceItemView.kt
│ │ ├── files
│ │ │ ├── FilesEvent.kt
│ │ │ ├── FilesUiState.kt
│ │ │ ├── FilesView.kt
│ │ │ └── FilesViewModel.kt
│ │ ├── main
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainEvent.kt
│ │ │ ├── MainNavigation.kt
│ │ │ ├── MainUiState.kt
│ │ │ ├── MainViewModel.kt
│ │ │ └── composables
│ │ │ │ ├── BottomNavBar.kt
│ │ │ │ └── SyncingDialog.kt
│ │ ├── settings
│ │ │ └── SettingsView.kt
│ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── utils
│ │ ├── Extensions.kt
│ │ └── FileUtils.kt
│ └── res
│ ├── drawable
│ ├── arrow_back_24.xml
│ ├── close_24.xml
│ ├── code_24.xml
│ ├── download_24.xml
│ ├── ic_launcher_foreground.xml
│ ├── person_24.xml
│ ├── refresh_24.xml
│ ├── select_all_24.xml
│ ├── settings_24.xml
│ ├── storage_24.xml
│ ├── sync_24.xml
│ └── usb_24.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values-es
│ └── strings.xml
│ ├── values
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /.idea
17 | /app/release
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tachisync
2 | Android app to sync Tachiyomi/Mihon/Others downloads on external devices (OTG only for now).
3 |
4 | I made this app for personal use to easily transfer the Tachiyomi downloads to my Kobo eBook, but feel free to request new features or improvements!
5 |
6 | Tested on Android 13+ but it should work fine on Android 10+.
7 | Untested on older Android versions, open an issue if it doesn’t work.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("org.jetbrains.kotlin.plugin.compose")
5 | }
6 |
7 | android {
8 | namespace = "com.axiel7.tachisync"
9 | compileSdk = 35
10 |
11 | defaultConfig {
12 | applicationId = "com.axiel7.tachisync"
13 | minSdk = 24
14 | targetSdk = 35
15 | versionCode = 11
16 | versionName = "1.0.10"
17 | setProperty("archivesBaseName", "tachisync-$versionName")
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | resourceConfigurations.addAll(arrayOf("en", "es"))
21 | vectorDrawables {
22 | useSupportLibrary = true
23 | }
24 | }
25 |
26 | buildTypes {
27 | debug {
28 | isDebuggable = true
29 | isMinifyEnabled = false
30 | isShrinkResources = false
31 | proguardFiles(
32 | getDefaultProguardFile("proguard-android-optimize.txt"),
33 | "proguard-rules.pro"
34 | )
35 | }
36 | release {
37 | isDebuggable = false
38 | isMinifyEnabled = true
39 | isShrinkResources = true
40 | proguardFiles(
41 | getDefaultProguardFile("proguard-android-optimize.txt"),
42 | "proguard-rules.pro"
43 | )
44 | }
45 | }
46 | compileOptions {
47 | sourceCompatibility = JavaVersion.VERSION_11
48 | targetCompatibility = JavaVersion.VERSION_11
49 | }
50 | kotlinOptions {
51 | jvmTarget = "11"
52 | }
53 | buildFeatures {
54 | compose = true
55 | buildConfig = true
56 | }
57 | packaging {
58 | resources {
59 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
60 | }
61 | }
62 | }
63 |
64 | dependencies {
65 |
66 | // Android X
67 | implementation("androidx.core:core-ktx:1.13.1")
68 | implementation("androidx.datastore:datastore-preferences:1.1.1")
69 | implementation("androidx.documentfile:documentfile:1.0.1")
70 |
71 | // Compose
72 | implementation(platform("androidx.compose:compose-bom:2024.08.00"))
73 | implementation("androidx.compose.ui:ui-tooling-preview")
74 | debugImplementation("androidx.compose.ui:ui-tooling")
75 |
76 | implementation("androidx.activity:activity-compose:1.9.1")
77 | implementation("androidx.compose.material3:material3-android:1.2.1")
78 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
79 | implementation("androidx.navigation:navigation-compose:2.7.7")
80 |
81 | implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
82 |
83 | // Test
84 | testImplementation("junit:junit:4.13.2")
85 | androidTestImplementation("androidx.test.ext:junit:1.2.1")
86 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
87 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
88 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/App.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.datastore.core.DataStore
6 | import androidx.datastore.preferences.core.Preferences
7 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.defaultPreferencesDataStore
8 |
9 | class App : Application() {
10 |
11 | init {
12 | INSTANCE = this
13 | }
14 |
15 | override fun onCreate() {
16 | super.onCreate()
17 | dataStore = defaultPreferencesDataStore
18 | }
19 |
20 | companion object {
21 | lateinit var INSTANCE: App
22 | private set
23 | val applicationContext: Context get() = INSTANCE.applicationContext
24 |
25 | lateinit var dataStore: DataStore
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/data/datastore/PreferencesDataStore.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.data.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.preferences.core.booleanPreferencesKey
5 | import androidx.datastore.preferences.core.stringPreferencesKey
6 | import androidx.datastore.preferences.preferencesDataStore
7 |
8 | object PreferencesDataStore {
9 |
10 | val EXTERNAL_URI_KEY = stringPreferencesKey("external_uri")
11 | val TACHIYOMI_URI_KEY = stringPreferencesKey("tachiyomi_uri")
12 | val REMOVE_SCANLATOR_KEY = booleanPreferencesKey("remove_scanlator")
13 |
14 | val Context.defaultPreferencesDataStore by preferencesDataStore(name = "default")
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/data/datastore/PreferencesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.data.datastore
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.edit
7 | import com.axiel7.tachisync.App
8 | import kotlinx.coroutines.flow.first
9 | import kotlinx.coroutines.flow.map
10 | import kotlinx.coroutines.runBlocking
11 |
12 | object PreferencesRepository {
13 |
14 | private val store get() = App.dataStore
15 |
16 | suspend fun get(key: Preferences.Key) = store.data.first()[key]
17 |
18 | @Composable
19 | fun remember(
20 | key: Preferences.Key,
21 | initial: T? = null
22 | ) = store.data.map { it[key] }.collectAsState(initial)
23 |
24 | /**
25 | * Gets the value by blocking the main thread
26 | */
27 | fun getSync(key: Preferences.Key) = runBlocking { get(key) }
28 |
29 | suspend fun set(
30 | key: Preferences.Key,
31 | value: T
32 | ) = store.edit { it[key] = value }
33 |
34 | /**
35 | * Sets the value by blocking the main thread
36 | */
37 | fun setSync(
38 | key: Preferences.Key,
39 | value: T
40 | ) = runBlocking { set(key, value) }
41 |
42 | suspend fun remove(key: Preferences.Key) = store.edit { it.remove(key) }
43 |
44 | /**
45 | * Removes the value by blocking the main thread
46 | */
47 | fun removeSync(key: Preferences.Key) = runBlocking { remove(key) }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/data/model/Manga.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.data.model
2 |
3 | import androidx.documentfile.provider.DocumentFile
4 |
5 | data class Manga(
6 | val name: String,
7 | val chapters: Int,
8 | val file: DocumentFile,
9 | val isSelected: Boolean = false
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.StateFlow
6 | import kotlinx.coroutines.flow.asStateFlow
7 | import kotlinx.coroutines.flow.update
8 |
9 | abstract class BaseViewModel : ViewModel(), UiEvent {
10 |
11 | protected abstract val mutableUiState: MutableStateFlow
12 | val uiState: StateFlow get() = mutableUiState.asStateFlow()
13 |
14 | @Suppress("UNCHECKED_CAST")
15 | fun setLoading(value: Boolean) {
16 | mutableUiState.update { it.setLoading(value) as S }
17 | }
18 |
19 | @Suppress("UNCHECKED_CAST")
20 | override fun showMessage(message: String?) {
21 | mutableUiState.update { it.setMessage(message) as S }
22 | }
23 |
24 | @Suppress("UNCHECKED_CAST")
25 | override fun onMessageDisplayed() {
26 | mutableUiState.update { it.setMessage(null) as S }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/base/UiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.base
2 |
3 | interface UiEvent {
4 | fun showMessage(message: String?)
5 | fun onMessageDisplayed()
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/base/UiState.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.base
2 |
3 | abstract class UiState {
4 | abstract val isLoading: Boolean
5 | abstract val message: String?
6 |
7 | // These methods are required because we can't have an abstract data class
8 | // so we need to manually implement the copy() method
9 |
10 | /**
11 | * copy(isLoading = value)
12 | */
13 | abstract fun setLoading(value: Boolean): UiState
14 |
15 | /**
16 | * copy(message = value)
17 | */
18 | abstract fun setMessage(value: String?): UiState
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/composables/MessageDialog.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.composables
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Text
5 | import androidx.compose.material3.TextButton
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 |
9 | @Composable
10 | fun MessageDialog(
11 | title: String?,
12 | message: String,
13 | onConfirm: () -> Unit,
14 | onDismiss: () -> Unit,
15 | ) {
16 | AlertDialog(
17 | onDismissRequest = onDismiss,
18 | title = title?.let { { Text(text = title) } },
19 | text = { Text(text = message) },
20 | confirmButton = {
21 | TextButton(onClick = onConfirm) {
22 | Text(text = stringResource(android.R.string.ok))
23 | }
24 | }
25 | )
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/external/ExternalEvent.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.external
2 |
3 | import android.content.Context
4 | import android.os.storage.StorageVolume
5 | import com.axiel7.tachisync.ui.base.UiEvent
6 |
7 | interface ExternalEvent : UiEvent {
8 | fun getExternalStorages(context: Context)
9 | fun onDeviceSelected(device: StorageVolume?)
10 | fun setOpenIntentForDirectory(value: Boolean)
11 | fun setOpenExternalDirectoryHelpDialog(value: Boolean)
12 | fun reset()
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/external/ExternalUiState.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.external
2 |
3 | import android.os.storage.StorageVolume
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.mutableStateListOf
6 | import androidx.compose.runtime.snapshots.SnapshotStateList
7 | import com.axiel7.tachisync.ui.base.UiState
8 |
9 | @Stable
10 | data class ExternalUiState(
11 | val externalStorages: SnapshotStateList = mutableStateListOf(),
12 | val selectedDevice: StorageVolume? = null,
13 | val openIntentForDirectory: Boolean = false,
14 | val openExternalDirectoryHelpDialog: Boolean = false,
15 | override val isLoading: Boolean = false,
16 | override val message: String? = null,
17 | ) : UiState() {
18 | override fun setLoading(value: Boolean) = copy(isLoading = value)
19 | override fun setMessage(value: String?) = copy(message = value)
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/external/ExternalView.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.external
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.lazy.LazyColumn
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.material3.Button
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.Surface
13 | import androidx.compose.material3.Text
14 | import androidx.compose.material3.TextButton
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.LaunchedEffect
17 | import androidx.compose.runtime.collectAsState
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.res.painterResource
23 | import androidx.compose.ui.res.stringResource
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import androidx.compose.ui.unit.dp
27 | import androidx.lifecycle.viewmodel.compose.viewModel
28 | import com.axiel7.tachisync.R
29 | import com.axiel7.tachisync.ui.composables.MessageDialog
30 | import com.axiel7.tachisync.ui.external.composables.ExternalDeviceItemView
31 | import com.axiel7.tachisync.ui.main.MainEvent
32 | import com.axiel7.tachisync.ui.main.MainUiState
33 | import com.axiel7.tachisync.ui.theme.TachisyncTheme
34 | import com.axiel7.tachisync.utils.FileUtils.areUriPermissionsGranted
35 | import com.axiel7.tachisync.utils.FileUtils.rememberUriLauncher
36 |
37 | const val EXTERNAL_STORAGE_DESTINATION = "external_storage"
38 |
39 | @Composable
40 | fun ExternalView(
41 | mainUiState: MainUiState,
42 | mainEvent: MainEvent?,
43 | modifier: Modifier = Modifier,
44 | ) {
45 | val viewModel: ExternalViewModel = viewModel()
46 | val uiState by viewModel.uiState.collectAsState()
47 |
48 | ExternalContent(
49 | mainUiState = mainUiState,
50 | mainEvent = mainEvent,
51 | externalUiState = uiState,
52 | externalEvent = viewModel,
53 | modifier = modifier,
54 | )
55 | }
56 |
57 | @Composable
58 | private fun ExternalContent(
59 | mainUiState: MainUiState,
60 | mainEvent: MainEvent?,
61 | externalUiState: ExternalUiState,
62 | externalEvent: ExternalEvent?,
63 | modifier: Modifier = Modifier,
64 | ) {
65 | val context = LocalContext.current
66 |
67 | val uriLauncher = rememberUriLauncher { uri ->
68 | mainEvent?.onExternalUriChanged(uri.toString())
69 | }
70 |
71 | LaunchedEffect(mainUiState.externalSyncUri) {
72 | if (mainUiState.externalSyncUri == null
73 | || !context.areUriPermissionsGranted(mainUiState.externalSyncUri.toString())
74 | ) {
75 | externalEvent?.getExternalStorages(context)
76 | }
77 | }
78 |
79 | if (externalUiState.openExternalDirectoryHelpDialog) {
80 | MessageDialog(
81 | title = stringResource(R.string.external_directory),
82 | message = stringResource(R.string.external_directory_explanation),
83 | onConfirm = {
84 | externalEvent?.setOpenExternalDirectoryHelpDialog(false)
85 | externalEvent?.setOpenIntentForDirectory(true)
86 | },
87 | onDismiss = { externalEvent?.setOpenExternalDirectoryHelpDialog(false) }
88 | )
89 | }
90 |
91 | if (externalUiState.openIntentForDirectory && externalUiState.selectedDevice != null) {
92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
93 | uriLauncher.launch(externalUiState.selectedDevice.createOpenDocumentTreeIntent())
94 | } else {
95 | @Suppress("DEPRECATION")
96 | externalUiState.selectedDevice.createAccessIntent(null)
97 | ?.let { uriLauncher.launch(it) }
98 | }
99 | externalEvent?.setOpenIntentForDirectory(false)
100 | }
101 |
102 | if (mainUiState.externalSyncUri != null) {
103 | Column(
104 | modifier = modifier.fillMaxSize(),
105 | verticalArrangement = Arrangement.Center,
106 | horizontalAlignment = Alignment.CenterHorizontally
107 | ) {
108 | Text(
109 | text = stringResource(
110 | R.string.contents_synced_on,
111 | mainUiState.externalSyncUri.lastPathSegment.orEmpty()
112 | ),
113 | modifier = Modifier.padding(horizontal = 16.dp),
114 | textAlign = TextAlign.Center
115 | )
116 |
117 | Button(
118 | onClick = {
119 | externalEvent?.reset()
120 | externalEvent?.getExternalStorages(context)
121 | },
122 | modifier = Modifier.padding(16.dp)
123 | ) {
124 | Text(text = stringResource(R.string.select_another_directory))
125 | }
126 | }
127 | } else if (externalUiState.externalStorages.isEmpty()) {
128 | Column(
129 | modifier = modifier.fillMaxSize(),
130 | verticalArrangement = Arrangement.Center,
131 | horizontalAlignment = Alignment.CenterHorizontally
132 | ) {
133 | Text(
134 | text = stringResource(R.string.no_external_found),
135 | modifier = Modifier.padding(16.dp)
136 | )
137 |
138 | TextButton(onClick = { externalEvent?.getExternalStorages(context) }) {
139 | Icon(
140 | painter = painterResource(R.drawable.refresh_24),
141 | contentDescription = stringResource(R.string.refresh)
142 | )
143 | Text(
144 | text = stringResource(R.string.refresh),
145 | modifier = Modifier.padding(start = 4.dp)
146 | )
147 | }
148 | }//:Column
149 | } else {
150 | Column(modifier = modifier) {
151 | Text(
152 | text = stringResource(R.string.select_device_to_sync),
153 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
154 | )
155 | LazyColumn {
156 | items(externalUiState.externalStorages) {
157 | ExternalDeviceItemView(
158 | deviceName = it.getDescription(context),
159 | onClick = {
160 | externalEvent?.onDeviceSelected(it)
161 | externalEvent?.setOpenExternalDirectoryHelpDialog(true)
162 | }
163 | )
164 | }
165 | }//:LazyColumn
166 | }//:Column
167 | }
168 | }
169 |
170 | @Preview(showBackground = true)
171 | @Composable
172 | fun ExternalPreview() {
173 | TachisyncTheme {
174 | Surface {
175 | ExternalContent(
176 | mainUiState = MainUiState(),
177 | mainEvent = null,
178 | externalUiState = ExternalUiState(),
179 | externalEvent = null,
180 | )
181 | }
182 | }
183 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/external/ExternalViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.external
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.os.Environment
6 | import android.os.storage.StorageManager
7 | import android.os.storage.StorageVolume
8 | import androidx.lifecycle.viewModelScope
9 | import com.axiel7.tachisync.App
10 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.EXTERNAL_URI_KEY
11 | import com.axiel7.tachisync.data.datastore.PreferencesRepository
12 | import com.axiel7.tachisync.ui.base.BaseViewModel
13 | import com.axiel7.tachisync.utils.FileUtils.releaseUriPermissions
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.update
17 | import kotlinx.coroutines.launch
18 |
19 | class ExternalViewModel : BaseViewModel(), ExternalEvent {
20 |
21 | override val mutableUiState = MutableStateFlow(ExternalUiState())
22 |
23 | override fun getExternalStorages(context: Context) {
24 | viewModelScope.launch(Dispatchers.IO) {
25 | setLoading(true)
26 | val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
27 | mutableUiState.value.run {
28 | externalStorages.clear()
29 | externalStorages.addAll(
30 | storageManager.storageVolumes
31 | .filter { it.isRemovable && it.state == Environment.MEDIA_MOUNTED }
32 | )
33 | }
34 | setLoading(false)
35 | }
36 | }
37 |
38 | override fun onDeviceSelected(device: StorageVolume?) {
39 | mutableUiState.update { it.copy(selectedDevice = device) }
40 | }
41 |
42 | override fun setOpenIntentForDirectory(value: Boolean) {
43 | mutableUiState.update { it.copy(openIntentForDirectory = value) }
44 | }
45 |
46 | override fun setOpenExternalDirectoryHelpDialog(value: Boolean) {
47 | mutableUiState.update { it.copy(openExternalDirectoryHelpDialog = value) }
48 | }
49 |
50 | override fun reset() {
51 | viewModelScope.launch(Dispatchers.IO) {
52 | PreferencesRepository.get(EXTERNAL_URI_KEY)?.let { uri ->
53 | App.applicationContext.releaseUriPermissions(Uri.parse(uri))
54 | PreferencesRepository.remove(EXTERNAL_URI_KEY)
55 | }
56 | mutableUiState.update {
57 | it.externalStorages.clear()
58 | it.copy(selectedDevice = null)
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/external/composables/ExternalDeviceItemView.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.external.composables
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.painterResource
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.text.style.TextOverflow
15 | import androidx.compose.ui.unit.dp
16 | import com.axiel7.tachisync.R
17 |
18 | @Composable
19 | fun ExternalDeviceItemView(
20 | deviceName: String,
21 | onClick: () -> Unit
22 | ) {
23 | Row(
24 | modifier = Modifier
25 | .clickable(onClick = onClick)
26 | .padding(16.dp)
27 | .fillMaxWidth(),
28 | verticalAlignment = Alignment.CenterVertically
29 | ) {
30 | Icon(
31 | painter = painterResource(R.drawable.usb_24),
32 | contentDescription = stringResource(R.string.device)
33 | )
34 | Text(
35 | text = deviceName,
36 | modifier = Modifier.padding(start = 8.dp),
37 | overflow = TextOverflow.Ellipsis,
38 | maxLines = 1
39 | )
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/files/FilesEvent.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.files
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import com.axiel7.tachisync.ui.base.UiEvent
6 |
7 | interface FilesEvent : UiEvent {
8 | fun onSelectedManga(index: Int, selected: Boolean)
9 | fun selectAllManga()
10 | fun deselectAllManga()
11 | fun refresh(context: Context)
12 | fun readDownloadsDir(downloadsUri: Uri, context: Context)
13 | fun setOpenIntentForDirectory(value: Boolean)
14 | fun setOpenTachiyomiDirectoryHelpDialog(value: Boolean)
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/files/FilesUiState.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.files
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.mutableStateListOf
5 | import androidx.compose.runtime.snapshots.SnapshotStateList
6 | import com.axiel7.tachisync.data.model.Manga
7 | import com.axiel7.tachisync.ui.base.UiState
8 |
9 | @Stable
10 | data class FilesUiState(
11 | val downloadedManga: SnapshotStateList = mutableStateListOf(),
12 | val selectedMangaIndices: SnapshotStateList = mutableStateListOf(),
13 | val openIntentForDirectory: Boolean = false,
14 | val openTachiyomiDirectoryHelpDialog: Boolean = false,
15 | override val isLoading: Boolean = false,
16 | override val message: String? = null
17 | ) : UiState() {
18 | override fun setLoading(value: Boolean) = copy(isLoading = value)
19 | override fun setMessage(value: String?) = copy(message = value)
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/files/FilesView.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.files
2 |
3 | import android.content.Intent
4 | import android.widget.Toast
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.foundation.lazy.itemsIndexed
15 | import androidx.compose.material3.Checkbox
16 | import androidx.compose.material3.CircularProgressIndicator
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Surface
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.LaunchedEffect
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.draw.clipToBounds
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.res.stringResource
31 | import androidx.compose.ui.text.style.TextOverflow
32 | import androidx.compose.ui.tooling.preview.Preview
33 | import androidx.compose.ui.unit.dp
34 | import com.axiel7.tachisync.R
35 | import com.axiel7.tachisync.data.model.Manga
36 | import com.axiel7.tachisync.ui.composables.MessageDialog
37 | import com.axiel7.tachisync.ui.main.MainEvent
38 | import com.axiel7.tachisync.ui.theme.TachisyncTheme
39 | import com.axiel7.tachisync.utils.FileUtils.rememberUriLauncher
40 |
41 | const val FILES_DESTINATION = "files"
42 |
43 | @Composable
44 | fun FilesView(
45 | filesUiState: FilesUiState,
46 | filesEvent: FilesEvent?,
47 | mainEvent: MainEvent?,
48 | contentPadding: PaddingValues,
49 | ) {
50 | val context = LocalContext.current
51 | val uriLauncher = rememberUriLauncher { uri ->
52 | mainEvent?.onTachiyomiUriChanged(uri.toString())
53 | filesEvent?.readDownloadsDir(uri, context)
54 | }
55 |
56 | if (filesUiState.openIntentForDirectory) {
57 | Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
58 | uriLauncher.launch(this)
59 | }
60 | filesEvent?.setOpenIntentForDirectory(false)
61 | }
62 |
63 | if (filesUiState.openTachiyomiDirectoryHelpDialog) {
64 | MessageDialog(
65 | title = stringResource(R.string.download_directory),
66 | message = stringResource(R.string.downloads_directory_explanation),
67 | onConfirm = {
68 | filesEvent?.setOpenTachiyomiDirectoryHelpDialog(false)
69 | filesEvent?.setOpenIntentForDirectory(true)
70 | },
71 | onDismiss = { filesEvent?.setOpenTachiyomiDirectoryHelpDialog(false) }
72 | )
73 | }
74 |
75 | if (filesUiState.message != null) {
76 | Toast.makeText(context, filesUiState.message, Toast.LENGTH_SHORT).show()
77 | filesEvent?.onMessageDisplayed()
78 | }
79 |
80 | LaunchedEffect(context) {
81 | if (filesUiState.downloadedManga.isEmpty()) filesEvent?.refresh(context)
82 | }
83 |
84 | Box(
85 | modifier = Modifier.clipToBounds()
86 | ) {
87 | LazyColumn(
88 | modifier = Modifier.fillMaxSize(),
89 | contentPadding = contentPadding
90 | ) {
91 | itemsIndexed(
92 | items = filesUiState.downloadedManga,
93 | key = { _, manga -> manga.file.uri }
94 | ) { index, manga ->
95 | SelectableMangaItemView(
96 | manga = manga,
97 | onClick = { selected ->
98 | filesEvent?.onSelectedManga(index, selected)
99 | }
100 | )
101 | }
102 | }
103 |
104 | if (filesUiState.isLoading) {
105 | CircularProgressIndicator(
106 | modifier = Modifier.align(Alignment.Center)
107 | )
108 | } else if (filesUiState.downloadedManga.isEmpty()) {
109 | Text(
110 | text = stringResource(R.string.no_downloaded_content),
111 | modifier = Modifier.align(Alignment.Center)
112 | )
113 | }
114 | }
115 | }
116 |
117 | @Composable
118 | fun SelectableMangaItemView(
119 | manga: Manga,
120 | onClick: (Boolean) -> Unit
121 | ) {
122 | var isChecked by remember { mutableStateOf(manga.isSelected) }
123 | Row(
124 | modifier = Modifier
125 | .clickable {
126 | isChecked = !manga.isSelected
127 | onClick(isChecked)
128 | }
129 | .padding(horizontal = 16.dp, vertical = 4.dp)
130 | .fillMaxWidth(),
131 | horizontalArrangement = Arrangement.SpaceBetween,
132 | verticalAlignment = Alignment.CenterVertically
133 | ) {
134 | Row(
135 | modifier = Modifier.weight(1f),
136 | verticalAlignment = Alignment.CenterVertically
137 | ) {
138 | Checkbox(
139 | checked = manga.isSelected,
140 | onCheckedChange = {
141 | isChecked = it
142 | onClick(it)
143 | }
144 | )
145 | Text(
146 | text = manga.name,
147 | modifier = Modifier.padding(8.dp),
148 | overflow = TextOverflow.Ellipsis,
149 | maxLines = 1
150 | )
151 | }
152 | Text(text = manga.chapters.toString(), color = MaterialTheme.colorScheme.onPrimaryContainer)
153 | }
154 | }
155 |
156 | @Preview(showBackground = true)
157 | @Composable
158 | fun FilesPreview() {
159 | TachisyncTheme {
160 | Surface {
161 | FilesView(
162 | filesUiState = FilesUiState(),
163 | filesEvent = null,
164 | mainEvent = null,
165 | contentPadding = PaddingValues()
166 | )
167 | }
168 | }
169 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/files/FilesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.files
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.documentfile.provider.DocumentFile
6 | import androidx.lifecycle.viewModelScope
7 | import com.axiel7.tachisync.App
8 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.TACHIYOMI_URI_KEY
9 | import com.axiel7.tachisync.data.datastore.PreferencesRepository
10 | import com.axiel7.tachisync.data.model.Manga
11 | import com.axiel7.tachisync.ui.base.BaseViewModel
12 | import com.axiel7.tachisync.utils.FileUtils.areUriPermissionsGranted
13 | import com.axiel7.tachisync.utils.FileUtils.releaseUriPermissions
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.update
17 | import kotlinx.coroutines.launch
18 |
19 | class FilesViewModel : BaseViewModel(), FilesEvent {
20 |
21 | override val mutableUiState = MutableStateFlow(FilesUiState())
22 |
23 | override fun onSelectedManga(index: Int, selected: Boolean) {
24 | mutableUiState.value.run {
25 | downloadedManga[index] = downloadedManga[index].copy(isSelected = selected)
26 |
27 | if (selected) selectedMangaIndices.add(index)
28 | else selectedMangaIndices.remove(index)
29 | }
30 | }
31 |
32 | override fun selectAllManga() {
33 | viewModelScope.launch(Dispatchers.IO) {
34 | mutableUiState.value.run {
35 | for (index in downloadedManga.indices) {
36 | downloadedManga[index] = downloadedManga[index].copy(isSelected = true)
37 | }
38 | selectedMangaIndices.clear()
39 | selectedMangaIndices.addAll(downloadedManga.indices)
40 | }
41 | }
42 | }
43 |
44 | override fun deselectAllManga() {
45 | viewModelScope.launch(Dispatchers.IO) {
46 | mutableUiState.value.run {
47 | for (index in downloadedManga.indices) {
48 | downloadedManga[index] = downloadedManga[index].copy(isSelected = false)
49 | }
50 | selectedMangaIndices.clear()
51 | }
52 | }
53 | }
54 |
55 | override fun refresh(context: Context) {
56 | viewModelScope.launch(Dispatchers.IO) {
57 | deselectAllManga()
58 | val tachiyomiUri = PreferencesRepository.get(TACHIYOMI_URI_KEY)
59 | if (tachiyomiUri.isNullOrEmpty() || !context.areUriPermissionsGranted(tachiyomiUri)) {
60 | setOpenTachiyomiDirectoryHelpDialog(true)
61 | } else {
62 | readDownloadsDir(Uri.parse(tachiyomiUri), context)
63 | }
64 | }
65 | }
66 |
67 | override fun readDownloadsDir(downloadsUri: Uri, context: Context) {
68 | viewModelScope.launch(Dispatchers.IO) {
69 | setLoading(true)
70 | val tempContent = mutableListOf()
71 | val sourcesDir = DocumentFile.fromTreeUri(context, downloadsUri)
72 | if (sourcesDir == null || !sourcesDir.exists()) {
73 | App.applicationContext.releaseUriPermissions(downloadsUri)
74 | PreferencesRepository.remove(TACHIYOMI_URI_KEY)
75 | } else {
76 | sourcesDir.listFiles().forEach { sourceFile ->
77 | sourceFile.listFiles().forEach { series ->
78 | val chaptersDownloaded = series.listFiles().size
79 | if (chaptersDownloaded > 0)
80 | tempContent.add(
81 | Manga(
82 | name = series.name ?: "Unknown",
83 | chapters = chaptersDownloaded,
84 | file = series
85 | )
86 | )
87 | }
88 | }
89 | mutableUiState.value.run {
90 | downloadedManga.clear()
91 | downloadedManga.addAll(tempContent)
92 | }
93 | }
94 |
95 | setLoading(false)
96 | }
97 | }
98 |
99 | override fun setOpenIntentForDirectory(value: Boolean) {
100 | mutableUiState.update { it.copy(openIntentForDirectory = value) }
101 | }
102 |
103 | override fun setOpenTachiyomiDirectoryHelpDialog(value: Boolean) {
104 | mutableUiState.update { it.copy(openTachiyomiDirectoryHelpDialog = value) }
105 | }
106 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.animation.AnimatedVisibility
8 | import androidx.compose.animation.scaleIn
9 | import androidx.compose.animation.scaleOut
10 | import androidx.compose.animation.slideInVertically
11 | import androidx.compose.animation.slideOutVertically
12 | import androidx.compose.foundation.layout.WindowInsets
13 | import androidx.compose.foundation.layout.WindowInsetsSides
14 | import androidx.compose.foundation.layout.fillMaxSize
15 | import androidx.compose.foundation.layout.only
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.systemBars
18 | import androidx.compose.material3.ExperimentalMaterial3Api
19 | import androidx.compose.material3.ExtendedFloatingActionButton
20 | import androidx.compose.material3.Icon
21 | import androidx.compose.material3.IconButton
22 | import androidx.compose.material3.MaterialTheme
23 | import androidx.compose.material3.Scaffold
24 | import androidx.compose.material3.SnackbarHost
25 | import androidx.compose.material3.SnackbarHostState
26 | import androidx.compose.material3.Surface
27 | import androidx.compose.material3.Text
28 | import androidx.compose.material3.TopAppBar
29 | import androidx.compose.material3.TopAppBarDefaults
30 | import androidx.compose.material3.surfaceColorAtElevation
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.runtime.LaunchedEffect
33 | import androidx.compose.runtime.collectAsState
34 | import androidx.compose.runtime.derivedStateOf
35 | import androidx.compose.runtime.getValue
36 | import androidx.compose.runtime.remember
37 | import androidx.compose.ui.Modifier
38 | import androidx.compose.ui.platform.LocalContext
39 | import androidx.compose.ui.res.painterResource
40 | import androidx.compose.ui.res.stringResource
41 | import androidx.compose.ui.tooling.preview.Preview
42 | import androidx.compose.ui.unit.dp
43 | import androidx.lifecycle.viewmodel.compose.viewModel
44 | import androidx.navigation.compose.currentBackStackEntryAsState
45 | import androidx.navigation.compose.rememberNavController
46 | import com.axiel7.tachisync.R
47 | import com.axiel7.tachisync.ui.files.FILES_DESTINATION
48 | import com.axiel7.tachisync.ui.files.FilesEvent
49 | import com.axiel7.tachisync.ui.files.FilesUiState
50 | import com.axiel7.tachisync.ui.files.FilesViewModel
51 | import com.axiel7.tachisync.ui.main.composables.BottomNavBar
52 | import com.axiel7.tachisync.ui.main.composables.SyncingDialog
53 | import com.axiel7.tachisync.ui.settings.SETTINGS_DESTINATION
54 | import com.axiel7.tachisync.ui.theme.TachisyncTheme
55 |
56 | class MainActivity : ComponentActivity() {
57 | override fun onCreate(savedInstanceState: Bundle?) {
58 | enableEdgeToEdge()
59 | super.onCreate(savedInstanceState)
60 | setContent {
61 | val mainViewModel: MainViewModel = viewModel()
62 | val mainUiState by mainViewModel.uiState.collectAsState()
63 | val filesViewModel: FilesViewModel = viewModel()
64 | val filesUiState by filesViewModel.uiState.collectAsState()
65 |
66 | TachisyncTheme {
67 | Surface(
68 | modifier = Modifier.fillMaxSize()
69 | ) {
70 | MainView(
71 | mainEvent = mainViewModel,
72 | mainUiState = mainUiState,
73 | filesEvent = filesViewModel,
74 | filesUiState = filesUiState,
75 | )
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
82 | @OptIn(ExperimentalMaterial3Api::class)
83 | @Composable
84 | fun MainView(
85 | mainEvent: MainEvent?,
86 | mainUiState: MainUiState,
87 | filesEvent: FilesEvent?,
88 | filesUiState: FilesUiState,
89 | ) {
90 | val context = LocalContext.current
91 | val navController = rememberNavController()
92 | val navBackStackEntry by navController.currentBackStackEntryAsState()
93 | val snackbarState = remember { SnackbarHostState() }
94 | val isFullScreenDestination by remember {
95 | derivedStateOf {
96 | navBackStackEntry?.destination?.route == SETTINGS_DESTINATION
97 | }
98 | }
99 | val showEditToolbar by remember {
100 | derivedStateOf {
101 | navBackStackEntry?.destination?.route == FILES_DESTINATION
102 | && filesUiState.selectedMangaIndices.isNotEmpty()
103 | }
104 | }
105 |
106 | Scaffold(
107 | topBar = {
108 | AnimatedVisibility(
109 | visible = !isFullScreenDestination,
110 | enter = slideInVertically(),
111 | exit = slideOutVertically()
112 | ) {
113 | TopAppBar(
114 | title = {
115 | if (showEditToolbar) {
116 | Text(
117 | text = stringResource(
118 | R.string.num_selected,
119 | filesUiState.selectedMangaIndices.size.toString()
120 | )
121 | )
122 | } else {
123 | Text(text = stringResource(R.string.app_name))
124 | }
125 | },
126 | navigationIcon = {
127 | if (showEditToolbar) {
128 | IconButton(onClick = { filesEvent?.deselectAllManga() }) {
129 | Icon(
130 | painter = painterResource(R.drawable.close_24),
131 | contentDescription = stringResource(R.string.close)
132 | )
133 | }
134 | }
135 | },
136 | actions = {
137 | if (showEditToolbar) {
138 | IconButton(onClick = { filesEvent?.selectAllManga() }) {
139 | Icon(
140 | painter = painterResource(R.drawable.select_all_24),
141 | contentDescription = stringResource(R.string.select_all)
142 | )
143 | }
144 | } else {
145 | if (navBackStackEntry?.destination?.route == FILES_DESTINATION) {
146 | IconButton(onClick = { filesEvent?.refresh(context) }) {
147 | Icon(
148 | painter = painterResource(R.drawable.refresh_24),
149 | contentDescription = stringResource(R.string.refresh)
150 | )
151 | }
152 | }
153 | IconButton(onClick = { navController.navigate(SETTINGS_DESTINATION) }) {
154 | Icon(
155 | painter = painterResource(R.drawable.settings_24),
156 | contentDescription = stringResource(R.string.settings)
157 | )
158 | }
159 | }
160 | },
161 | colors = TopAppBarDefaults.topAppBarColors(
162 | containerColor = if (showEditToolbar)
163 | MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
164 | else MaterialTheme.colorScheme.background
165 | )
166 | )
167 | }
168 | },
169 | bottomBar = {
170 | AnimatedVisibility(
171 | visible = !isFullScreenDestination,
172 | enter = slideInVertically { it / 2 },
173 | exit = slideOutVertically { it / 2 }
174 | ) {
175 | BottomNavBar(navController = navController)
176 | }
177 | },
178 | snackbarHost = {
179 | SnackbarHost(hostState = snackbarState)
180 | },
181 | floatingActionButton = {
182 | AnimatedVisibility(
183 | visible = !isFullScreenDestination && showEditToolbar,
184 | enter = scaleIn(),
185 | exit = scaleOut()
186 | ) {
187 | ExtendedFloatingActionButton(
188 | onClick = {
189 | mainEvent?.syncContents(
190 | context,
191 | filesUiState.downloadedManga,
192 | filesUiState.selectedMangaIndices
193 | )
194 | }
195 | ) {
196 | Icon(
197 | painter = painterResource(R.drawable.sync_24),
198 | contentDescription = stringResource(R.string.sync)
199 | )
200 | Text(
201 | text = stringResource(R.string.sync),
202 | modifier = Modifier.padding(start = 8.dp)
203 | )
204 | }
205 | }
206 | },
207 | contentWindowInsets = WindowInsets.systemBars
208 | .only(WindowInsetsSides.Horizontal)
209 | ) { padding ->
210 | MainNavigation(
211 | navController = navController,
212 | padding = padding,
213 | mainUiState = mainUiState,
214 | mainEvent = mainEvent,
215 | filesUiState = filesUiState,
216 | filesEvent = filesEvent,
217 | )
218 | }
219 |
220 | LaunchedEffect(mainUiState.message, filesUiState.message) {
221 | if (mainUiState.message != null) {
222 | snackbarState.showSnackbar(message = mainUiState.message)
223 | mainEvent?.onMessageDisplayed()
224 | } else if (filesUiState.message != null) {
225 | snackbarState.showSnackbar(message = filesUiState.message)
226 | filesEvent?.onMessageDisplayed()
227 | }
228 | }
229 |
230 | if (mainUiState.isLoading) {
231 | SyncingDialog(
232 | progress = mainUiState.syncProgress
233 | )
234 | }
235 | }
236 |
237 | @Preview(showBackground = true)
238 | @Composable
239 | fun DefaultPreview() {
240 | TachisyncTheme {
241 | Surface {
242 | MainView(
243 | mainEvent = null,
244 | mainUiState = MainUiState(),
245 | filesEvent = null,
246 | filesUiState = FilesUiState(),
247 | )
248 | }
249 | }
250 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/MainEvent.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main
2 |
3 | import android.content.Context
4 | import com.axiel7.tachisync.data.model.Manga
5 | import com.axiel7.tachisync.ui.base.UiEvent
6 |
7 | interface MainEvent : UiEvent {
8 | fun onExternalUriChanged(value: String)
9 | fun onTachiyomiUriChanged(value: String)
10 | fun syncContents(context: Context, contents: List, selected: List)
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/MainNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main
2 |
3 | import androidx.compose.animation.core.animateDpAsState
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.calculateEndPadding
9 | import androidx.compose.foundation.layout.calculateStartPadding
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalLayoutDirection
15 | import androidx.navigation.NavHostController
16 | import androidx.navigation.compose.NavHost
17 | import androidx.navigation.compose.composable
18 | import com.axiel7.tachisync.ui.external.EXTERNAL_STORAGE_DESTINATION
19 | import com.axiel7.tachisync.ui.external.ExternalView
20 | import com.axiel7.tachisync.ui.files.FILES_DESTINATION
21 | import com.axiel7.tachisync.ui.files.FilesEvent
22 | import com.axiel7.tachisync.ui.files.FilesUiState
23 | import com.axiel7.tachisync.ui.files.FilesView
24 | import com.axiel7.tachisync.ui.settings.SETTINGS_DESTINATION
25 | import com.axiel7.tachisync.ui.settings.SettingsView
26 |
27 | @Composable
28 | fun MainNavigation(
29 | navController: NavHostController,
30 | padding: PaddingValues,
31 | mainUiState: MainUiState,
32 | mainEvent: MainEvent?,
33 | filesUiState: FilesUiState,
34 | filesEvent: FilesEvent?,
35 | ) {
36 | val topPadding by animateDpAsState(
37 | targetValue = padding.calculateTopPadding(),
38 | label = "top_bar_padding"
39 | )
40 | val bottomPadding by animateDpAsState(
41 | targetValue = padding.calculateBottomPadding(),
42 | label = "bottom_bar_padding"
43 | )
44 | NavHost(
45 | navController = navController,
46 | startDestination = FILES_DESTINATION,
47 | modifier = Modifier.padding(
48 | start = padding.calculateStartPadding(LocalLayoutDirection.current),
49 | end = padding.calculateEndPadding(LocalLayoutDirection.current),
50 | ),
51 | enterTransition = { fadeIn(tween(400)) },
52 | exitTransition = { fadeOut(tween(400)) }
53 | ) {
54 | composable(FILES_DESTINATION) {
55 | FilesView(
56 | filesUiState = filesUiState,
57 | filesEvent = filesEvent,
58 | mainEvent = mainEvent,
59 | contentPadding = PaddingValues(
60 | top = topPadding,
61 | bottom = bottomPadding
62 | )
63 | )
64 | }
65 |
66 | composable(EXTERNAL_STORAGE_DESTINATION) {
67 | ExternalView(
68 | mainUiState = mainUiState,
69 | mainEvent = mainEvent,
70 | modifier = Modifier.padding(
71 | top = topPadding,
72 | bottom = bottomPadding
73 | )
74 | )
75 | }
76 |
77 | composable(SETTINGS_DESTINATION) {
78 | SettingsView(
79 | resetDownloadsDirectory = {
80 | mainEvent?.onTachiyomiUriChanged("")
81 | },
82 | navigateBack = {
83 | navController.popBackStack()
84 | }
85 | )
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/MainUiState.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main
2 |
3 | import android.net.Uri
4 | import com.axiel7.tachisync.ui.base.UiState
5 |
6 | data class MainUiState(
7 | val syncProgress: Float = 0f,
8 | val externalSyncUri: Uri? = null,
9 | override val isLoading: Boolean = false,
10 | override val message: String? = null
11 | ) : UiState() {
12 | override fun setLoading(value: Boolean) = copy(isLoading = value)
13 | override fun setMessage(value: String?) = copy(message = value)
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.compose.runtime.mutableIntStateOf
6 | import androidx.documentfile.provider.DocumentFile
7 | import androidx.lifecycle.viewModelScope
8 | import com.axiel7.tachisync.App
9 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.EXTERNAL_URI_KEY
10 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.REMOVE_SCANLATOR_KEY
11 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.TACHIYOMI_URI_KEY
12 | import com.axiel7.tachisync.data.datastore.PreferencesRepository
13 | import com.axiel7.tachisync.data.model.Manga
14 | import com.axiel7.tachisync.ui.base.BaseViewModel
15 | import com.axiel7.tachisync.utils.FileUtils.syncDirectory
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.flow.MutableStateFlow
18 | import kotlinx.coroutines.flow.launchIn
19 | import kotlinx.coroutines.flow.map
20 | import kotlinx.coroutines.flow.onEach
21 | import kotlinx.coroutines.flow.update
22 | import kotlinx.coroutines.launch
23 |
24 | class MainViewModel : BaseViewModel(), MainEvent {
25 |
26 | override val mutableUiState = MutableStateFlow(MainUiState())
27 |
28 | override fun onExternalUriChanged(value: String) {
29 | viewModelScope.launch {
30 | PreferencesRepository.set(EXTERNAL_URI_KEY, value)
31 | }
32 | }
33 |
34 | override fun onTachiyomiUriChanged(value: String) {
35 | viewModelScope.launch {
36 | PreferencesRepository.set(TACHIYOMI_URI_KEY, value)
37 | }
38 | }
39 |
40 | override fun syncContents(context: Context, contents: List, selected: List) {
41 | viewModelScope.launch(Dispatchers.IO) {
42 | val externalUri = uiState.value.externalSyncUri
43 | if (selected.isEmpty()) {
44 | showMessage("No content selected")
45 | } else if (externalUri == null) {
46 | showMessage("No external directory selected")
47 | } else {
48 | mutableUiState.update { it.copy(isLoading = true, syncProgress = 0f) }
49 | try {
50 | val destDir = DocumentFile.fromTreeUri(context, externalUri)
51 | if (destDir == null || !destDir.isDirectory) {
52 | showMessage("Invalid external directory")
53 | } else {
54 | val shouldRemoveScanlator = PreferencesRepository.get(REMOVE_SCANLATOR_KEY)
55 | val currentFileCount = mutableIntStateOf(0)
56 | val selectedContent =
57 | contents.filterIndexed { index, _ -> selected.contains(index) }
58 | val files = selectedContent.map { it.file }
59 | val total = selectedContent.sumOf { it.chapters }.toFloat()
60 | files.forEach { file ->
61 | context.syncDirectory(
62 | sourceDir = file,
63 | destRootDir = destDir,
64 | progress = uiState.value.syncProgress,
65 | updateProgress = this@MainViewModel::updateProgress,
66 | currentFileCount = currentFileCount,
67 | total = total,
68 | shouldRemoveScanlator = shouldRemoveScanlator == true
69 | )
70 | }
71 | }
72 | } catch (e: Exception) {
73 | mutableUiState.update {
74 | it.copy(isLoading = false, message = e.message ?: "Error syncing")
75 | }
76 | return@launch
77 | }
78 | mutableUiState.update { it.copy(isLoading = false, message = "Sync completed") }
79 | }
80 | }
81 | }
82 |
83 | private fun updateProgress(value: Float) {
84 | mutableUiState.update { it.copy(syncProgress = value) }
85 | }
86 |
87 | init {
88 | App.dataStore.data
89 | .map { it[EXTERNAL_URI_KEY]?.let { uri -> Uri.parse(uri) } }
90 | .onEach { value ->
91 | mutableUiState.update { it.copy(externalSyncUri = value) }
92 | }
93 | .launchIn(viewModelScope)
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/composables/BottomNavBar.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main.composables
2 |
3 | import androidx.compose.material3.Icon
4 | import androidx.compose.material3.NavigationBar
5 | import androidx.compose.material3.NavigationBarItem
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableIntStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.res.painterResource
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.navigation.NavController
15 | import androidx.navigation.NavGraph.Companion.findStartDestination
16 | import com.axiel7.tachisync.R
17 | import com.axiel7.tachisync.ui.external.EXTERNAL_STORAGE_DESTINATION
18 | import com.axiel7.tachisync.ui.files.FILES_DESTINATION
19 |
20 | @Composable
21 | fun BottomNavBar(navController: NavController) {
22 | var selectedItem by remember { mutableIntStateOf(0) }
23 |
24 | NavigationBar {
25 | NavigationBarItem(
26 | selected = selectedItem == 0,
27 | onClick = {
28 | selectedItem = 0
29 | navController.navigate(FILES_DESTINATION) {
30 | popUpTo(navController.graph.findStartDestination().id) {
31 | saveState = true
32 | }
33 | launchSingleTop = true
34 | restoreState = true
35 | }
36 | },
37 | icon = {
38 | Icon(
39 | painter = painterResource(R.drawable.download_24),
40 | contentDescription = stringResource(R.string.downloads)
41 | )
42 | },
43 | label = { Text(text = stringResource(R.string.downloads)) }
44 | )
45 |
46 | NavigationBarItem(
47 | selected = selectedItem == 1,
48 | onClick = {
49 | selectedItem = 1
50 | navController.navigate(EXTERNAL_STORAGE_DESTINATION) {
51 | popUpTo(navController.graph.findStartDestination().id) {
52 | saveState = true
53 | }
54 | launchSingleTop = true
55 | restoreState = true
56 | }
57 | },
58 | icon = {
59 | Icon(
60 | painter = painterResource(R.drawable.storage_24),
61 | contentDescription = stringResource(R.string.external)
62 | )
63 | },
64 | label = { Text(text = stringResource(R.string.external)) }
65 | )
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/main/composables/SyncingDialog.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.main.composables
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.LinearProgressIndicator
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.unit.dp
11 | import com.axiel7.tachisync.R
12 |
13 | @Composable
14 | fun SyncingDialog(progress: Float) {
15 | AlertDialog(
16 | onDismissRequest = { },
17 | confirmButton = { },
18 | title = {
19 | Text(
20 | text = stringResource(R.string.syncing),
21 | modifier = Modifier.padding(16.dp)
22 | )
23 | },
24 | text = {
25 | LinearProgressIndicator(
26 | progress = { progress },
27 | modifier = Modifier.padding(8.dp)
28 | )
29 | }
30 | )
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/settings/SettingsView.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.settings
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.IconButton
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.material3.Switch
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TopAppBar
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.rememberCoroutineScope
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.res.painterResource
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.tooling.preview.Preview
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import com.axiel7.tachisync.BuildConfig
32 | import com.axiel7.tachisync.R
33 | import com.axiel7.tachisync.data.datastore.PreferencesDataStore.REMOVE_SCANLATOR_KEY
34 | import com.axiel7.tachisync.data.datastore.PreferencesRepository
35 | import com.axiel7.tachisync.ui.theme.TachisyncTheme
36 | import com.axiel7.tachisync.utils.Extensions.openAction
37 | import kotlinx.coroutines.launch
38 |
39 | const val SETTINGS_DESTINATION = "settings"
40 |
41 | @OptIn(ExperimentalMaterial3Api::class)
42 | @Composable
43 | fun SettingsView(
44 | resetDownloadsDirectory: () -> Unit,
45 | navigateBack: () -> Unit
46 | ) {
47 | val context = LocalContext.current
48 | val scope = rememberCoroutineScope()
49 | val removeScanlator by PreferencesRepository.remember(REMOVE_SCANLATOR_KEY)
50 | Scaffold(
51 | topBar = {
52 | TopAppBar(
53 | title = { Text(text = stringResource(R.string.settings)) },
54 | navigationIcon = {
55 | IconButton(onClick = navigateBack) {
56 | Icon(
57 | painter = painterResource(R.drawable.arrow_back_24),
58 | contentDescription = stringResource(R.string.back)
59 | )
60 | }
61 | }
62 | )
63 | }
64 | ) {
65 | Column(
66 | modifier = Modifier.padding(it)
67 | ) {
68 | SwitchPreference(
69 | title = "Remove scanlator from filename",
70 | value = removeScanlator,
71 | onValueChange = {
72 | scope.launch {
73 | PreferencesRepository.set(REMOVE_SCANLATOR_KEY, it)
74 | }
75 | }
76 | )
77 | AboutItem(
78 | title = stringResource(R.string.reset_downloads_directory),
79 | icon = R.drawable.sync_24,
80 | onClick = resetDownloadsDirectory
81 | )
82 | AboutItem(
83 | title = "GitHub",
84 | subtitle = "Source code",
85 | icon = R.drawable.code_24,
86 | onClick = { context.openAction("https://github.com/axiel7/Tachisync") }
87 | )
88 |
89 | AboutItem(
90 | title = stringResource(R.string.developer),
91 | subtitle = "axiel7",
92 | icon = R.drawable.person_24,
93 | onClick = { context.openAction("https://github.com/axiel7") }
94 | )
95 |
96 | AboutItem(
97 | title = stringResource(R.string.version),
98 | subtitle = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
99 | )
100 | }
101 | }
102 | }
103 |
104 | @Composable
105 | fun AboutItem(
106 | title: String,
107 | modifier: Modifier = Modifier,
108 | subtitle: String? = null,
109 | @DrawableRes icon: Int? = null,
110 | onClick: (() -> Unit)? = null,
111 | ) {
112 | Row(
113 | modifier = modifier
114 | .fillMaxWidth()
115 | .clickable { onClick?.invoke() },
116 | horizontalArrangement = Arrangement.Start,
117 | verticalAlignment = Alignment.CenterVertically
118 | ) {
119 | if (icon != null) {
120 | Icon(
121 | painter = painterResource(icon),
122 | contentDescription = "",
123 | modifier = Modifier.padding(16.dp),
124 | tint = MaterialTheme.colorScheme.primary
125 | )
126 | } else {
127 | Spacer(
128 | modifier = Modifier
129 | .padding(16.dp)
130 | .size(24.dp)
131 | )
132 | }
133 |
134 | Column(
135 | modifier = if (subtitle != null)
136 | Modifier.padding(16.dp)
137 | else Modifier.padding(horizontal = 16.dp)
138 | ) {
139 | Text(
140 | text = title,
141 | color = MaterialTheme.colorScheme.onSurface
142 | )
143 |
144 | if (subtitle != null) {
145 | Text(
146 | text = subtitle,
147 | color = MaterialTheme.colorScheme.onSurfaceVariant,
148 | fontSize = 13.sp
149 | )
150 | }
151 | }
152 | }
153 | }
154 |
155 | @Composable
156 | fun SwitchPreference(
157 | title: String,
158 | modifier: Modifier = Modifier,
159 | subtitle: String? = null,
160 | value: Boolean?,
161 | @DrawableRes icon: Int? = null,
162 | onValueChange: (Boolean) -> Unit
163 | ) {
164 | Row(
165 | modifier = modifier
166 | .fillMaxWidth()
167 | .clickable {
168 | onValueChange(value?.not() ?: false)
169 | },
170 | horizontalArrangement = Arrangement.SpaceBetween,
171 | verticalAlignment = Alignment.CenterVertically
172 | ) {
173 | Row(
174 | modifier = Modifier.weight(1f),
175 | horizontalArrangement = Arrangement.Start,
176 | verticalAlignment = Alignment.CenterVertically
177 | ) {
178 | if (icon != null) {
179 | Icon(
180 | painter = painterResource(icon),
181 | contentDescription = title,
182 | modifier = Modifier.padding(16.dp),
183 | tint = MaterialTheme.colorScheme.primary
184 | )
185 | } else {
186 | Spacer(
187 | modifier = Modifier
188 | .padding(16.dp)
189 | .size(24.dp)
190 | )
191 | }
192 |
193 | Column(
194 | modifier = if (subtitle != null)
195 | Modifier.padding(16.dp)
196 | else Modifier.padding(horizontal = 16.dp)
197 | ) {
198 | Text(
199 | text = title,
200 | color = MaterialTheme.colorScheme.onSurface
201 | )
202 |
203 | if (subtitle != null) {
204 | Text(
205 | text = subtitle,
206 | color = MaterialTheme.colorScheme.onSurfaceVariant,
207 | fontSize = 13.sp,
208 | lineHeight = 14.sp
209 | )
210 | }
211 | }//: Column
212 | }//: Row
213 |
214 | Switch(
215 | checked = value ?: false,
216 | onCheckedChange = onValueChange,
217 | modifier = Modifier.padding(horizontal = 16.dp)
218 | )
219 | }//: Row
220 | }
221 |
222 | @Preview(showBackground = true)
223 | @Composable
224 | fun SettingsPreview() {
225 | TachisyncTheme {
226 | SettingsView(
227 | resetDownloadsDirectory = {},
228 | navigateBack = {}
229 | )
230 | }
231 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val md_theme_light_primary = Color(0xFF006398)
6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
7 | val md_theme_light_primaryContainer = Color(0xFFCDE5FF)
8 | val md_theme_light_onPrimaryContainer = Color(0xFF001D31)
9 | val md_theme_light_secondary = Color(0xFF51606F)
10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
11 | val md_theme_light_secondaryContainer = Color(0xFFD4E4F6)
12 | val md_theme_light_onSecondaryContainer = Color(0xFF0D1D2A)
13 | val md_theme_light_tertiary = Color(0xFF006398)
14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
15 | val md_theme_light_tertiaryContainer = Color(0xFFCDE5FF)
16 | val md_theme_light_onTertiaryContainer = Color(0xFF001D31)
17 | val md_theme_light_error = Color(0xFFBA1A1A)
18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
19 | val md_theme_light_onError = Color(0xFFFFFFFF)
20 | val md_theme_light_onErrorContainer = Color(0xFF410002)
21 | val md_theme_light_background = Color(0xFFFCFCFF)
22 | val md_theme_light_onBackground = Color(0xFF1A1C1E)
23 | val md_theme_light_surface = Color(0xFFFCFCFF)
24 | val md_theme_light_onSurface = Color(0xFF1A1C1E)
25 | val md_theme_light_surfaceVariant = Color(0xFFDEE3EB)
26 | val md_theme_light_onSurfaceVariant = Color(0xFF42474E)
27 | val md_theme_light_outline = Color(0xFF72787E)
28 | val md_theme_light_inverseOnSurface = Color(0xFFF0F0F4)
29 | val md_theme_light_inverseSurface = Color(0xFF2F3033)
30 | val md_theme_light_inversePrimary = Color(0xFF93CCFF)
31 | val md_theme_light_shadow = Color(0xFF000000)
32 | val md_theme_light_surfaceTint = Color(0xFF006398)
33 | val md_theme_light_outlineVariant = Color(0xFFC2C7CE)
34 | val md_theme_light_scrim = Color(0xFF000000)
35 |
36 | val md_theme_dark_primary = Color(0xFF93CCFF)
37 | val md_theme_dark_onPrimary = Color(0xFF003351)
38 | val md_theme_dark_primaryContainer = Color(0xFF004B74)
39 | val md_theme_dark_onPrimaryContainer = Color(0xFFCDE5FF)
40 | val md_theme_dark_secondary = Color(0xFFB8C8DA)
41 | val md_theme_dark_onSecondary = Color(0xFF23323F)
42 | val md_theme_dark_secondaryContainer = Color(0xFF394857)
43 | val md_theme_dark_onSecondaryContainer = Color(0xFFD4E4F6)
44 | val md_theme_dark_tertiary = Color(0xFF93CCFF)
45 | val md_theme_dark_onTertiary = Color(0xFF003351)
46 | val md_theme_dark_tertiaryContainer = Color(0xFF004B74)
47 | val md_theme_dark_onTertiaryContainer = Color(0xFFCDE5FF)
48 | val md_theme_dark_error = Color(0xFFFFB4AB)
49 | val md_theme_dark_errorContainer = Color(0xFF93000A)
50 | val md_theme_dark_onError = Color(0xFF690005)
51 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
52 | val md_theme_dark_background = Color(0xFF1A1C1E)
53 | val md_theme_dark_onBackground = Color(0xFFE2E2E5)
54 | val md_theme_dark_surface = Color(0xFF1A1C1E)
55 | val md_theme_dark_onSurface = Color(0xFFE2E2E5)
56 | val md_theme_dark_surfaceVariant = Color(0xFF42474E)
57 | val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CE)
58 | val md_theme_dark_outline = Color(0xFF8C9198)
59 | val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E)
60 | val md_theme_dark_inverseSurface = Color(0xFFE2E2E5)
61 | val md_theme_dark_inversePrimary = Color(0xFF006398)
62 | val md_theme_dark_shadow = Color(0xFF000000)
63 | val md_theme_dark_surfaceTint = Color(0xFF93CCFF)
64 | val md_theme_dark_outlineVariant = Color(0xFF42474E)
65 | val md_theme_dark_scrim = Color(0xFF000000)
66 |
67 |
68 | val seed = Color(0xFF2E84BF)
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val LightColorScheme = lightColorScheme(
14 | primary = md_theme_light_primary,
15 | onPrimary = md_theme_light_onPrimary,
16 | primaryContainer = md_theme_light_primaryContainer,
17 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
18 | secondary = md_theme_light_secondary,
19 | onSecondary = md_theme_light_onSecondary,
20 | secondaryContainer = md_theme_light_secondaryContainer,
21 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
22 | tertiary = md_theme_light_tertiary,
23 | onTertiary = md_theme_light_onTertiary,
24 | tertiaryContainer = md_theme_light_tertiaryContainer,
25 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
26 | error = md_theme_light_error,
27 | errorContainer = md_theme_light_errorContainer,
28 | onError = md_theme_light_onError,
29 | onErrorContainer = md_theme_light_onErrorContainer,
30 | background = md_theme_light_background,
31 | onBackground = md_theme_light_onBackground,
32 | surface = md_theme_light_surface,
33 | onSurface = md_theme_light_onSurface,
34 | surfaceVariant = md_theme_light_surfaceVariant,
35 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
36 | outline = md_theme_light_outline,
37 | inverseOnSurface = md_theme_light_inverseOnSurface,
38 | inverseSurface = md_theme_light_inverseSurface,
39 | inversePrimary = md_theme_light_inversePrimary,
40 | surfaceTint = md_theme_light_surfaceTint,
41 | outlineVariant = md_theme_light_outlineVariant,
42 | scrim = md_theme_light_scrim,
43 | )
44 |
45 |
46 | private val DarkColorScheme = darkColorScheme(
47 | primary = md_theme_dark_primary,
48 | onPrimary = md_theme_dark_onPrimary,
49 | primaryContainer = md_theme_dark_primaryContainer,
50 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
51 | secondary = md_theme_dark_secondary,
52 | onSecondary = md_theme_dark_onSecondary,
53 | secondaryContainer = md_theme_dark_secondaryContainer,
54 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
55 | tertiary = md_theme_dark_tertiary,
56 | onTertiary = md_theme_dark_onTertiary,
57 | tertiaryContainer = md_theme_dark_tertiaryContainer,
58 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
59 | error = md_theme_dark_error,
60 | errorContainer = md_theme_dark_errorContainer,
61 | onError = md_theme_dark_onError,
62 | onErrorContainer = md_theme_dark_onErrorContainer,
63 | background = md_theme_dark_background,
64 | onBackground = md_theme_dark_onBackground,
65 | surface = md_theme_dark_surface,
66 | onSurface = md_theme_dark_onSurface,
67 | surfaceVariant = md_theme_dark_surfaceVariant,
68 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
69 | outline = md_theme_dark_outline,
70 | inverseOnSurface = md_theme_dark_inverseOnSurface,
71 | inverseSurface = md_theme_dark_inverseSurface,
72 | inversePrimary = md_theme_dark_inversePrimary,
73 | surfaceTint = md_theme_dark_surfaceTint,
74 | outlineVariant = md_theme_dark_outlineVariant,
75 | scrim = md_theme_dark_scrim,
76 | )
77 |
78 | @Composable
79 | fun TachisyncTheme(
80 | darkTheme: Boolean = isSystemInDarkTheme(),
81 | // Dynamic color is available on Android 12+
82 | dynamicColor: Boolean = true,
83 | content: @Composable () -> Unit
84 | ) {
85 | val colorScheme = when {
86 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
87 | val context = LocalContext.current
88 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
89 | }
90 | darkTheme -> DarkColorScheme
91 | else -> LightColorScheme
92 | }
93 |
94 | MaterialTheme(
95 | colorScheme = colorScheme,
96 | typography = Typography,
97 | content = content
98 | )
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.utils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 |
7 | object Extensions {
8 |
9 | fun Context.openAction(uri: String) {
10 | Intent(Intent.ACTION_VIEW, Uri.parse(uri)).apply {
11 | startActivity(this)
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/axiel7/tachisync/utils/FileUtils.kt:
--------------------------------------------------------------------------------
1 | package com.axiel7.tachisync.utils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import androidx.activity.compose.rememberLauncherForActivityResult
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.MutableIntState
10 | import androidx.documentfile.provider.DocumentFile
11 | import com.axiel7.tachisync.App
12 |
13 | object FileUtils {
14 |
15 | private val chapterRegex = Regex("(#\\d*)")
16 |
17 | @Composable
18 | fun rememberUriLauncher(onUriReceived: (Uri) -> Unit) = rememberLauncherForActivityResult(
19 | ActivityResultContracts.StartActivityForResult()
20 | ) {
21 | it.data?.data?.let { uri ->
22 | App.applicationContext.contentResolver.takePersistableUriPermission(
23 | uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or
24 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
25 | )
26 | onUriReceived(uri)
27 | }
28 | }
29 |
30 | fun Context.areUriPermissionsGranted(uriString: String): Boolean {
31 | val list = contentResolver.persistedUriPermissions
32 | for (index in list.indices) {
33 | val persistedUriString = list[index].uri.toString()
34 | if (persistedUriString == uriString && list[index].isWritePermission && list[index].isReadPermission) {
35 | return true
36 | }
37 | }
38 | return false
39 | }
40 |
41 | fun Context.releaseUriPermissions(uri: Uri) {
42 | val flags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
43 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
44 | contentResolver.releasePersistableUriPermission(uri, flags)
45 | }
46 |
47 | fun Context.syncDirectory(
48 | sourceDir: DocumentFile,
49 | destRootDir: DocumentFile,
50 | progress: Float,
51 | updateProgress: (Float) -> Unit,
52 | currentFileCount: MutableIntState,
53 | total: Float,
54 | shouldRemoveScanlator: Boolean,
55 | ) {
56 | if (sourceDir.isDirectory && destRootDir.isDirectory && sourceDir.name != null) {
57 | if (sourceDir.name!!.endsWith("_tmp")) return
58 | // Check if the directory already exist
59 | var destDir = destRootDir.findFile(sourceDir.name!!)
60 | if (destDir == null) {
61 | // Create the destination directory if it doesn't exist
62 | destDir = destRootDir.createDirectory(sourceDir.name!!)
63 | }
64 |
65 | // Copy each file or directory to the destination directory
66 | sourceDir.listFiles().forEach { file ->
67 | file.name?.let { filename ->
68 | if (file.isDirectory) {
69 | // If the file is a directory, recursively call this function
70 | val childSourceDir = sourceDir.findFile(file.name!!)
71 | val childDestDir = destDir?.findFile(file.name!!)
72 | if (childSourceDir != null && childDestDir != null)
73 | syncDirectory(
74 | sourceDir = childSourceDir,
75 | destRootDir = childDestDir,
76 | progress = progress,
77 | updateProgress = updateProgress,
78 | currentFileCount = currentFileCount,
79 | total = total,
80 | shouldRemoveScanlator = shouldRemoveScanlator
81 | )
82 | } else {
83 | currentFileCount.intValue += 1
84 | // If the file is a regular file, copy its contents to the destination file
85 | // Check if the file already exist
86 | var destFile = destDir?.findFile(filename)
87 | if (destFile == null && file.type != null) {
88 | destFile = destDir?.createFile(file.type!!, file.name!!)
89 | }
90 | if (shouldRemoveScanlator && filename.contains('_')) {
91 | val chapterNumber = chapterRegex.find(filename)?.value
92 | val nameWithoutScanlator = filename
93 | .replaceBefore('_', "")
94 | .drop(1)
95 | val finalName = if (chapterNumber != null
96 | && !nameWithoutScanlator.contains(chapterNumber)
97 | ) {
98 | "$chapterNumber $nameWithoutScanlator"
99 | } else {
100 | nameWithoutScanlator
101 | }
102 | destFile?.renameTo(finalName)
103 | }
104 | if (destFile?.uri != null) {
105 | val inputStream = contentResolver.openInputStream(file.uri)
106 | val outputStream = contentResolver.openOutputStream(destFile.uri)
107 | if (outputStream != null) inputStream?.copyTo(outputStream)
108 | inputStream?.close()
109 | outputStream?.close()
110 | }
111 | updateProgress(currentFileCount.intValue.div(total))
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/close_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/code_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/download_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
14 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/person_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/refresh_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/select_all_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/storage_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/sync_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/usb_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | %s seleccionados
4 | Cerrar
5 | Seleccionar Todo
6 | Sincronizar
7 | Sincronizando…
8 | Descargas
9 | Externo
10 | Directorio de descargas
11 | En la siguiente pantalla, dirígete a la carpeta contenedora de tus descargas de Tachiyomi/Mihon (normalmente /Tachiyomi/downloads/) y pulsa \'Usar esta carpeta\'
12 | Los contenidos serán sincronizados en: %s
13 | Seleccionar otro directorio
14 | No se han encontrado dispositivos externos
15 | Refrescar
16 | Selecciona el dispositivo donde sincronizar
17 | Dispositivo
18 | Directorio externo
19 | En la siguiente pantalla, dirígete a la carpeta donde quieras sincronizar tu contenido descargado y pulsa \'Usar esta carpeta\'
20 | Ajustes
21 | Atrás
22 | Desarrollador
23 | Versión
24 | Resetear directorio de descargas
25 | Sin contenido descargado
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2E84BF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Tachisync
3 | %s selected
4 | Close
5 | Select All
6 | Sync now
7 | Syncing…
8 | Downloads
9 | External
10 | Downloads directory
11 | On the next screen, navigate to the folder containing your Tachiyomi/Mihon downloads (usually /Tachiyomi/downloads/) and select \'Use this folder\'
12 | Contents will be synced on: %s
13 | Select another directory
14 | No external devices found
15 | Refresh
16 | Select the device you want to sync on
17 | Device
18 | External directory
19 | On the next screen, navigate to the folder you want to sync your downloaded content and select \'Use this folder\'
20 | Settings
21 | Back
22 | Developer
23 | Version
24 | Reset downloads directory
25 | No downloaded content
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | val kotlinVersion = "2.0.20"
4 | id("com.android.application") version "8.5.2" apply false
5 | id("org.jetbrains.kotlin.android") version kotlinVersion apply false
6 | id("org.jetbrains.kotlin.plugin.compose") version kotlinVersion apply false
7 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiel7/Tachisync/d8784f039594c0e93799503cabb3150bcf0096bf/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Apr 01 13:35:47 JST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Tachisync"
16 | include(":app")
17 |
--------------------------------------------------------------------------------