diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2bd2f21..111c0ac 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -20,7 +20,7 @@ jobs: - name: set up JDK 23 uses: actions/setup-java@v4 with: - java-version: '23' + java-version: '24' distribution: 'temurin' cache: gradle diff --git a/Images/gk-library-view-translations.png b/Images/gk-library-view-translations.png new file mode 100644 index 0000000..1fb95b7 Binary files /dev/null and b/Images/gk-library-view-translations.png differ diff --git a/Images/gk-library-view.png b/Images/gk-library-view.png new file mode 100644 index 0000000..70a3952 Binary files /dev/null and b/Images/gk-library-view.png differ diff --git a/Images/gk-reading-view-translation.png b/Images/gk-reading-view-translation.png new file mode 100644 index 0000000..7651ea8 Binary files /dev/null and b/Images/gk-reading-view-translation.png differ diff --git a/Images/gk-reading-view.png b/Images/gk-reading-view.png new file mode 100644 index 0000000..9e6d324 Binary files /dev/null and b/Images/gk-reading-view.png differ diff --git a/Images/library-lazy-cards.png b/Images/library-lazy-cards.png new file mode 100644 index 0000000..3fa22ed Binary files /dev/null and b/Images/library-lazy-cards.png differ diff --git a/Images/library-view-translations.png b/Images/library-view-translations.png new file mode 100644 index 0000000..eea0ac4 Binary files /dev/null and b/Images/library-view-translations.png differ diff --git a/Images/reading-view-list.png b/Images/reading-view-list.png new file mode 100644 index 0000000..e140ec8 Binary files /dev/null and b/Images/reading-view-list.png differ diff --git a/LatinReader/compose/build.gradle b/LatinReader/compose/build.gradle index 67f6dec..50c0cbf 100644 --- a/LatinReader/compose/build.gradle +++ b/LatinReader/compose/build.gradle @@ -1,16 +1,15 @@ plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'org.jetbrains.kotlin.plugin.compose' + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") id("kotlin-parcelize") } android { - namespace 'com.telpirion.compose' + namespace "com.telpirion.compose" compileSdkVersion(36) defaultConfig { - applicationId "com.telpirion.compose" minSdk 24 targetSdk 36 versionCode 1 @@ -22,12 +21,12 @@ android { buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" } } kotlin { jvmToolchain { - languageVersion = JavaLanguageVersion.of("24") + languageVersion = JavaLanguageVersion.of("17") } } buildFeatures { @@ -36,31 +35,36 @@ android { } dependencies { - implementation project(':core') + implementation project(":core") - implementation platform('androidx.compose:compose-bom:2025.12.00') - implementation("androidx.activity:activity-compose:1.12.1") + implementation platform("androidx.compose:compose-bom:2025.12.01") + implementation("androidx.activity:activity-compose:1.12.2") implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3-window-size-class") implementation("androidx.compose.material3.adaptive:adaptive:1.2.0") implementation("androidx.compose.material3.adaptive:adaptive-layout:1.2.0") implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.2.0") - implementation 'androidx.compose.material3:material3' + implementation("androidx.compose.material3:material3") implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") - implementation 'androidx.compose.ui:ui' - implementation 'androidx.compose.ui:ui-graphics' - implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.core:core-ktx:1.17.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.10.0' + implementation "androidx.compose.ui:ui" + implementation "androidx.compose.ui:ui-graphics" + implementation "androidx.compose.ui:ui-tooling-preview" + implementation "androidx.core:core-ktx:1.17.0" + implementation "androidx.datastore:datastore-preferences:1.2.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.10.0" + implementation("androidx.preference:preference-ktx:1.2.1") + implementation 'androidx.navigation:navigation-compose:2.9.6' + + implementation("com.github.mukeshsolanki:MarkdownView-Android:2.0.0") - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' - androidTestImplementation platform('androidx.compose:compose-bom:2025.12.00') - androidTestImplementation 'androidx.compose.ui:ui-test-junit4' - debugImplementation 'androidx.compose.ui:ui-tooling' - debugImplementation 'androidx.compose.ui:ui-test-manifest' + testImplementation "junit:junit:4.13.2" + androidTestImplementation "androidx.test.ext:junit:1.3.0" + androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0" + androidTestImplementation platform("androidx.compose:compose-bom:2025.12.01") + androidTestImplementation "androidx.compose.ui:ui-test-junit4" + debugImplementation "androidx.compose.ui:ui-tooling" + debugImplementation "androidx.compose.ui:ui-test-manifest" } \ No newline at end of file diff --git a/LatinReader/compose/src/main/AndroidManifest.xml b/LatinReader/compose/src/main/AndroidManifest.xml index f31abad..69fc412 100644 --- a/LatinReader/compose/src/main/AndroidManifest.xml +++ b/LatinReader/compose/src/main/AndroidManifest.xml @@ -1,24 +1,3 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/MainActivity.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/MainActivity.kt index e6a5017..decd13b 100644 --- a/LatinReader/compose/src/main/java/com/telpirion/compose/MainActivity.kt +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/MainActivity.kt @@ -1,21 +1,34 @@ package com.telpirion.compose +import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import com.ericmschmidt.classicsreader.data.placeholders.PseudoLibrary -import com.telpirion.compose.ui.components.NavigableListDetailPaneScaffoldFull -import com.telpirion.compose.ui.theme.LatinReaderTheme +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.telpirion.compose.ui.ReaderApp +import com.telpirion.compose.ui.theme.ReaderTheme +@Suppress("unused") +val Context.dataStore: DataStore by preferencesDataStore(name = "settings", + produceMigrations = { context -> + listOf(SharedPreferencesMigration(context, "settings")) + }) class MainActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - val library = PseudoLibrary() setContent { - LatinReaderTheme { - NavigableListDetailPaneScaffoldFull(library = library) + ReaderTheme { + val windowSizeClass = + calculateWindowSizeClass(this) + ReaderApp(windowSizeClass = windowSizeClass) } } } diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/ReaderApp.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/ReaderApp.kt new file mode 100644 index 0000000..7b4e3d2 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/ReaderApp.kt @@ -0,0 +1,378 @@ +@file:Suppress("unused") + +package com.telpirion.compose.ui + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.telpirion.compose.R +import com.telpirion.compose.ui.components.ReaderAppNavHost +import com.telpirion.compose.ui.components.ReaderTopAppBar +import com.telpirion.compose.ui.components.Screen +import com.telpirion.compose.ui.components.navOptionsBuilder +import kotlinx.coroutines.launch +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.ericmschmidt.classicsreader.MyApplication +import com.telpirion.compose.viewmodels.DictionaryViewModel +import com.telpirion.compose.ui.theme.ReaderTheme +import androidx.navigation.NavHostController +import com.ericmschmidt.classicsreader.data.Library +import androidx.compose.ui.tooling.preview.Preview +import com.ericmschmidt.classicsreader.data.PreferencesDataStore +import com.ericmschmidt.classicsreader.data.PreferencesState +import com.ericmschmidt.classicsreader.data.placeholders.PseudoLibrary +import com.telpirion.compose.viewmodels.DictionaryUiState + +private val bottomNavigationItems = listOf( + Screen.Library, + Screen.Recent, + Screen.Settings +) + +private val navigationItems = listOf( + Screen.Library, + Screen.Recent, + Screen.Translation, + Screen.Vocab, + Screen.Help, + Screen.Info, + Screen.Settings, +) + +// Global declaration for user settings preferences +val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +@SuppressLint("FlowOperatorInvokedInComposition") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderApp( + windowSizeClass: WindowSizeClass +) { + val navController = rememberNavController() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + + // Determine if the layout is compact. On non-compact layouts, a navigation rail is shown. + val isCompact = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact + + // Get the current back stack entry to determine the selected route. + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + // Instantiate the activity-scoped ViewModel + val dictionaryViewModel: DictionaryViewModel = viewModel( + factory = DictionaryViewModel.Factory + ) + val dictionaryUiState by dictionaryViewModel.uiState.collectAsStateWithLifecycle() + + // Get recently read from preferences + val context = LocalContext.current + val preferencesDataStore = remember(context) { PreferencesDataStore(context) } + + val preferences = preferencesDataStore.preferencesFlow().collectAsState( + initial = PreferencesState() + ).value + val currentWorkId = preferences.recentlyRead + + val navigationFunc : (String) -> Unit = { route -> + when (route){ + Screen.Recent.route -> { + navController.navigate( + route = Screen.Recent.createRoute(currentWorkId, false), + builder = navOptionsBuilder(navController)) + } + else -> navController.navigate(route, navOptionsBuilder(navController)) + } + } + + val manifest = MyApplication.applicationInstance().library + + ReaderAppContent( + isCompact = isCompact, + navController = navController, + drawerState = drawerState, + dictionaryUiState = dictionaryUiState, + onQueryChange = { text -> dictionaryViewModel.onQueryChange(text) }, + onSearch = { + dictionaryViewModel.search(dictionaryUiState.searchQuery) + navController.navigate(Screen.Dictionary.createRoute()) + }, + onClearSearch = dictionaryViewModel::clearSearch, + currentWorkId = currentWorkId, + currentRoute = currentRoute, + library = manifest, + onNavigate = navigationFunc, + content = { modifier -> + ReaderAppNavHost( + navController = navController, + modifier = modifier, + dictionaryViewModel = dictionaryViewModel + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderAppContent( + isCompact: Boolean, + navController: NavHostController, + drawerState: DrawerState, + dictionaryUiState: DictionaryUiState, + onQueryChange: (String) -> Unit, + onSearch: () -> Unit, + onClearSearch: () -> Unit, + currentWorkId: String?, + currentRoute: String?, + library: Library, + onNavigate: (String) -> Unit, + content: @Composable (Modifier) -> Unit +) { + ReaderTheme { + val scope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerState = drawerState, + gesturesEnabled = isCompact, + drawerContent = { + NavDrawerContent( + currentRoute = currentRoute, + onNavigate = { route -> + onNavigate(route) + scope.launch { drawerState.close() } + }, + library = library + ) + } + ) { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + topBar = { + ReaderTopAppBar( + onMenuClick = { scope.launch { drawerState.open() } }, + searchText = dictionaryUiState.searchQuery, + onSearchTextChange = onQueryChange, + onSearch = onSearch, + onClearSearch = onClearSearch + ) + }, + bottomBar = { + // Show bottom navigation bar only on compact screens + if (isCompact) { + ReaderBottomNavigationBar( + currentRoute = currentRoute, + onNavigate = { route -> + if (drawerState.isClosed) { + onNavigate(route) + } + } + ) + } + } + ) { innerPadding -> + Row( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + if (!isCompact) { + ReaderNavigationRail( + currentRoute = currentRoute, + onNavigate = { route -> + if (drawerState.isClosed) { + onNavigate(route) + } + } + ) + } + content(Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun NavDrawerContent( + currentRoute: String?, + onNavigate: (String) -> Unit, + modifier: Modifier = Modifier, + library: Library +) { + ModalDrawerSheet(modifier = modifier) { + NavigationHeader(modifier = Modifier.padding(16.dp), library = library) + Spacer(Modifier.height(12.dp)) + navigationItems.forEach { screen -> + NavigationDrawerItem( + icon = { Icon(screen.icon, contentDescription = null) }, + label = { Text(stringResource(screen.label)) }, + selected = currentRoute == screen.route, + onClick = { onNavigate(screen.route) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + } + } +} + +@Composable +private fun NavigationHeader( + modifier: Modifier = Modifier, + library: Library +) { + Column(modifier = modifier) { + // A Row to neatly arrange the logo and app name side-by-side. + Row(verticalAlignment = Alignment.CenterVertically) { + library.GetHeaderIcon() + Spacer(Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.titleMedium + ) + } + } +} + +@Composable +private fun ReaderBottomNavigationBar( + currentRoute: String?, + onNavigate: (String) -> Unit +) { + NavigationBar { + bottomNavigationItems.forEach { screen -> + NavigationBarItem( + icon = { Icon(screen.icon, contentDescription = null) }, + label = { Text(stringResource(screen.label)) }, + selected = currentRoute?.startsWith(screen.route.substringBefore("/")) ?: false, + onClick = { + // The 'when' expression now correctly handles each navigation case. + val route = when (screen) { + is Screen.Recent -> { + // In a real app, you would get the last-read work ID from a ViewModel. + val recentWorkId = "" // Placeholder ID + screen.createRoute(recentWorkId, false) + } + is Screen.Library -> screen.createRoute() + is Screen.Settings -> screen.createRoute() + // Add any other specific cases from bottomNavigationItems here. + else -> Screen.Library.createRoute() + } + onNavigate(route) + } + ) + } + } +} + +@Composable +private fun ReaderNavigationRail( + currentRoute: String?, + onNavigate: (String) -> Unit +) { + NavigationRail { + navigationItems.forEach { screen -> + NavigationRailItem( + icon = { Icon(screen.icon, contentDescription = null) }, + label = { Text(stringResource(screen.label)) }, + selected = currentRoute == screen.route, + onClick = { onNavigate(screen.route) } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true, name = "Reader App Preview Compact") +@Composable +fun ReaderAppPreview() { + val navController = rememberNavController() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val library = PseudoLibrary() + + ReaderAppContent( + isCompact = true, + navController = navController, + drawerState = drawerState, + dictionaryUiState = DictionaryUiState(), + onQueryChange = {}, + onSearch = {}, + onClearSearch = {}, + currentWorkId = null, + currentRoute = Screen.Library.route, + library = library, + onNavigate = {}, + content = { + Box(Modifier.fillMaxSize()) { + Text("NavHost Content") + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true, widthDp = 840, name = "Reader App Preview Expanded") +@Composable +fun ReaderAppExpandedPreview() { + val navController = rememberNavController() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val library = PseudoLibrary() + + ReaderAppContent( + isCompact = false, + navController = navController, + drawerState = drawerState, + dictionaryUiState = DictionaryUiState(), + onQueryChange = {}, + onSearch = {}, + onClearSearch = {}, + currentWorkId = null, + currentRoute = Screen.Library.route, + library = library, + onNavigate = {}, + content = { + Box(Modifier.fillMaxSize()) { + Text("NavHost Content") + } + } + ) +} diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/DetailsPane.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/DetailsPane.kt new file mode 100644 index 0000000..c18ec3b --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/DetailsPane.kt @@ -0,0 +1,198 @@ +package com.telpirion.compose.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults.buttonColors +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Devices.PIXEL +import androidx.compose.ui.tooling.preview.Devices.TABLET +import androidx.compose.ui.tooling.preview.Devices.TV_1080p +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.ericmschmidt.classicsreader.data.Library +import com.ericmschmidt.classicsreader.data.WorkInfo +import com.ericmschmidt.classicsreader.ui.components.LibraryPreviewProvider +import com.telpirion.compose.ui.theme.TelpirionGray + + +@Preview(showBackground = true, device = PIXEL) +@Composable +fun DetailsPanePreviewPixel( + @PreviewParameter(LibraryPreviewProvider::class) library: Library +) { + val works = library.getWorks() + val selectedItem = SelectedItem(0) + DetailsPane(item = selectedItem, works = works, onReadClick = {}, onDismiss = {}) +} + +@Preview(showBackground = true, device = TABLET) +@Composable +fun DetailsPanePreviewTablet( + @PreviewParameter(LibraryPreviewProvider::class) library: Library +) { + val works = library.getWorks() + val selectedItem = SelectedItem(0) + DetailsPane(item = selectedItem, works = works, onReadClick = {}, onDismiss = {}) +} + +@Preview(showBackground = true, device = TV_1080p) +@Composable +fun DetailsPanePreviewTV( + @PreviewParameter(LibraryPreviewProvider::class) library: Library +) { + val works = library.getWorks() + val selectedItem = SelectedItem(0) + DetailsPane(item = selectedItem, works = works, onReadClick = {}, onDismiss = {}) +} + +@Composable +fun BoldedText(boldText: String, normalText: String) { + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(boldText) + } + append(": ") + append(normalText) + } + ) +} + +@Composable +fun DetailsPane( + item: SelectedItem, + works: Array, + onReadClick: (String) -> Unit, + onDismiss: () -> Unit +) { + val workInfo = works.getOrNull(item.id) + + Box(modifier = Modifier.fillMaxSize()) { + Card( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.12f), + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) + ) { + if (workInfo != null) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), // Inner padding for the content + horizontalAlignment = Alignment.Start + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Image( + painter = painterResource(id = workInfo.image), + contentDescription = "${workInfo.title} cover art", + modifier = Modifier.size(120.dp) + ) + Column( + modifier = Modifier.padding(start = 16.dp) + ) { + // 2. Title + Text( + text = workInfo.title, + style = MaterialTheme.typography.headlineSmall + ) + + Spacer(Modifier.height(8.dp)) + + // 3. Author + Text( + text = workInfo.author, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + } + + Spacer(Modifier.height(24.dp)) + Button( + onClick = { onReadClick(workInfo.id) }, + modifier = Modifier + .width(120.dp), + colors = buttonColors( + containerColor = TelpirionGray, + contentColor = Color.White + ) + ) { + Text("Read") + } + Spacer(Modifier.height(24.dp)) + HorizontalDivider( + thickness = 2.dp, + color = MaterialTheme.colorScheme.secondary + ) + Spacer(Modifier.height(24.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + BoldedText("Translator", + workInfo.translator + ) + BoldedText("Editor", + workInfo.editor + ) + BoldedText("Description", + workInfo.description + ) + } + } + } else { + // A placeholder view for when no item is selected. + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("Select a work from the list.") + } + } + } + IconButton( + onClick = onDismiss, + modifier = Modifier.align(Alignment.TopEnd).padding(16.dp) + ) { + Icon(Icons.Default.Close, contentDescription = "Close") + } + } +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ListDetailPane.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ListDetailPane.kt new file mode 100644 index 0000000..c8996bc --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ListDetailPane.kt @@ -0,0 +1,120 @@ +package com.telpirion.compose.ui.components + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.navigation.NavController +import com.ericmschmidt.classicsreader.MyApplication +import com.ericmschmidt.classicsreader.data.WorkInfo +import com.ericmschmidt.classicsreader.ui.components.PrettyCardLazyList +import com.ericmschmidt.classicsreader.ui.components.PrettyCardLazyVerticalGrid +import com.ericmschmidt.classicsreader.data.DISPLAY_TYPE +import com.ericmschmidt.classicsreader.data.DISPLAY_TYPE_DEFAULT +import com.telpirion.compose.ui.dataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@Preview +@Composable +fun NavigableListDetailPaneScaffoldFullPreview( +) { + // The preview doesn't have a NavController, so the button won't navigate. + ListDetailPane(navController = null) +} + +@SuppressLint("FlowOperatorInvokedInComposition") +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun ListDetailPane( + modifier: Modifier = Modifier, + navController: NavController? = null, + screen: Screen = Screen.Library, + onDismiss: () -> Unit = {} +) { + + val context : Context = MyApplication.applicationInstance().context + val library = MyApplication.applicationInstance().library + val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + val works = library.getWorks() + val isTranslation = screen == Screen.Translation + + // Get display type from preferences + val displayTypeKey = stringPreferencesKey(DISPLAY_TYPE) + val displayTypeValue: Flow = context.dataStore.data + .map { preferences -> preferences[displayTypeKey] ?: DISPLAY_TYPE_DEFAULT } + + val onItemClick : (workInfo: WorkInfo) -> Unit = { workInfo -> + // Find the index of the clicked work to maintain compatibility + // with SelectedItem(id: Int). + val index = works.indexOf(workInfo) + if (index != -1) { + // Navigate to the detail pane with the passed item + scope.launch { + scaffoldNavigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, + SelectedItem(index) + ) + } + } + } + + NavigableListDetailPaneScaffold( + navigator = scaffoldNavigator, + listPane = { + AnimatedPane { + if (displayTypeValue.collectAsState( + initial = DISPLAY_TYPE_DEFAULT).value == "Grid") { + PrettyCardLazyVerticalGrid( + library = library, + modifier = modifier, + onCardClick = onItemClick, + isTranslation = isTranslation + ) + } else { + PrettyCardLazyList( + library = library, + modifier = modifier, + onRowClick = onItemClick, + isTranslation = isTranslation + ) + } + } + }, + detailPane = { + AnimatedPane { + // Show the detail pane content if an item is selected + scaffoldNavigator.currentDestination?.contentKey?.let { selectedItem -> + DetailsPane( + item = selectedItem, + works = works, + onReadClick = { workId -> + when (screen) { + Screen.Translation -> navController?.navigate(Screen.Recent.createRoute(workId, true)) + else -> navController?.navigate(Screen.Recent.createRoute(workId, false)) + } + + }, + onDismiss = { + scope.launch { + scaffoldNavigator.navigateBack() + onDismiss() + } + } + ) + } + } + }, + ) +} diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ReaderAppNavHost.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ReaderAppNavHost.kt new file mode 100644 index 0000000..8a54ba4 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ReaderAppNavHost.kt @@ -0,0 +1,201 @@ +@file:Suppress("unused", "UnusedVariable") + +package com.telpirion.compose.ui.components + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.Book +import androidx.compose.material.icons.filled.Bookmark +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.School +import androidx.compose.material.icons.filled.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.NavType +import androidx.navigation.navArgument +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.telpirion.compose.MainActivity +import com.telpirion.compose.ui.screens.MarkdownScreen +import com.telpirion.compose.ui.screens.ReadingScreen +import com.telpirion.compose.ui.screens.SettingsScreen +import com.telpirion.compose.viewmodels.DictionaryViewModel +import com.ericmschmidt.classicsreader.R as CoreResources + +/** + * Defines the primary navigation destinations in the app. + */ +open class Screen(val route: String, val label: Int, val icon: ImageVector) { + object Library : Screen( + "library/", + CoreResources.string.nav_drawer_library, + Icons.Default.Book + ) { + fun createRoute() = "library/" + } + object Recent : Screen( + "recent/{workId}/{isTranslation}", + CoreResources.string.nav_drawer_recent, + Icons.Default.Bookmark + ) { + fun createRoute(workId: String?, isTranslation: Boolean?) = "recent/$workId/$isTranslation" + } + object Translation : Screen( + "translation/", + CoreResources.string.nav_drawer_translations, + Icons.Default.Description + ) { + fun createRoute() = "translation/" + } + object Vocab : Screen( + "vocab/", + CoreResources.string.nav_drawer_vocab, + Icons.Default.School + ) { + fun createRoute() = "vocab/" + } + + object Dictionary : Screen( + "dictionary/", + CoreResources.string.nav_drawer_dictionary, + Icons.Default.Description + ) { + fun createRoute() = "dictionary/" + } + + object Settings : Screen( + "settings/", + CoreResources.string.action_settings, + Icons.Default.Settings + ) { + fun createRoute() = "settings/" + } + object Help : Screen( + "help/", + CoreResources.string.nav_drawer_help, + Icons.AutoMirrored.Filled.Help + ) { + fun createRoute() = "help/" + } + object Info : Screen( + "info/", + CoreResources.string.nav_drawer_info, + Icons.Default.Info + ) { + fun createRoute() = "info/" + } +} + +// NavOptions builder function +fun navOptionsBuilder(navController: NavHostController): NavOptionsBuilder.() -> Unit { + return { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } +} + +@Composable +fun ReaderAppNavHost( + modifier: Modifier = Modifier, + context: Context = LocalContext.current, + dictionaryViewModel: DictionaryViewModel = viewModel( + viewModelStoreOwner = (context as MainActivity) + ), + navController: NavHostController, +) { + + NavHost( + navController = navController, + startDestination = Screen.Library.route, + modifier = modifier + ) { + composable( + Screen.Library.route, + ) { backStackEntry -> + // Pass the NavController here + ListDetailPane(navController = navController) + } + + composable( + Screen.Translation.route, + ) { backStackEntry -> + // And here as well + ListDetailPane(navController = navController, screen = Screen.Translation) + } + + composable( + route = Screen.Recent.route, + arguments = listOf( + navArgument("workId") { type = NavType.StringType }, + navArgument("isTranslation") { type = NavType.BoolType }) + ) { backStackEntry -> + + // TODO: Pull from PreferencesDataStore? + val workId = backStackEntry.arguments?.getString("workId") + val isTranslation = backStackEntry.arguments?.getBoolean("isTranslation") + ReadingScreen( + workId = workId, + isTranslation = isTranslation ?: false, + navController = navController, + ) + } + + composable ( + Screen.Vocab.route + ) { backStackEntry -> + LaunchedEffect(Unit) { + dictionaryViewModel.getVocab() + } + ReadingScreen( + workId = "", + isTranslation = false, + navController = navController, + screen = Screen.Vocab + ) + } + + composable ( + Screen.Dictionary.route + ) { backStackEntry -> + ReadingScreen( + workId = "test", + isTranslation = false, + navController = navController, + screen = Screen.Dictionary + ) + + } + + composable ( + Screen.Help.route + ) { backStackEntry -> + MarkdownScreen(screen = Screen.Help) + } + + composable ( + Screen.Info.route + ) { backStackEntry -> + MarkdownScreen(screen = Screen.Info) + } + + + + composable( + Screen.Settings.route, + ) { backStackEntry -> + SettingsScreen() + } + } +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ReaderTopAppBar.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ReaderTopAppBar.kt new file mode 100644 index 0000000..aa0ae81 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/ReaderTopAppBar.kt @@ -0,0 +1,102 @@ +package com.telpirion.compose.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.telpirion.compose.R +import com.ericmschmidt.classicsreader.R as CoreR + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.input.ImeAction + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderTopAppBar( + onMenuClick: () -> Unit, + searchText: String, + onSearchTextChange: (String) -> Unit, + onSearch: () -> Unit, + onClearSearch: () -> Unit, + modifier: Modifier = Modifier +) { + val keyboardController = LocalSoftwareKeyboardController.current + + // A single TopAppBar that always shows the search bar. + TopAppBar( + title = { + TextField( + value = searchText, + onValueChange = onSearchTextChange, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { + onSearch() + keyboardController?.hide() // Hide keyboard on search + }), + placeholder = { Text(stringResource(R.string.search_label)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null + ) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(32.dp), // Makes the TextField pill-shaped + colors = TextFieldDefaults.colors( + // Set a distinct background color for the TextField + focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, + disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant, + // Hide the indicator line for a cleaner look + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) + ) + }, + navigationIcon = { + IconButton(onClick = onMenuClick) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = stringResource(R.string.cd_open_navigation_drawer) + ) + } + }, + actions = { + if (searchText.isNotEmpty()) { + IconButton(onClick = onClearSearch) { // Use the new callback + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.cd_clear_search) + ) + } + } + }, + // Set the background color for the entire TopAppBar. + // This will contrast with the TextField's 'surfaceVariant' color. + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ), + modifier = modifier + ) +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/SupportingPane.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/SupportingPane.kt new file mode 100644 index 0000000..fb71e27 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/components/SupportingPane.kt @@ -0,0 +1,181 @@ +@file:Suppress("SpellCheckingInspection") + +package com.telpirion.compose.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.ericmschmidt.classicsreader.data.TOCEntry +import com.telpirion.compose.viewmodels.ReadingViewModel +import com.telpirion.compose.R +import com.telpirion.compose.ui.theme.LatinReaderTheme + +class TOCEntryParameterProvider : PreviewParameterProvider> { + override val values = sequenceOf( + listOf( + TOCEntry("Book Primus", 0, 0), + TOCEntry("Book Secundus", 1, 0), + TOCEntry("Book Tertius", 2, 0) + ) + ) +} + +@Preview +@Composable +fun TableOfContentsPanePreview( + @PreviewParameter(TOCEntryParameterProvider::class) toc: List +) { + TableOfContentsPane(toc, onTocEntryClick = {}, onClose = {}) +} + +@Preview +@Composable +fun TranslationPanePreview() { + TranslationPane( + onClose = {}, + translationContent = "Content", + translationInfo = "Author, Title" + ) +} + +@Composable +fun TableOfContentsPane( + viewModel: ReadingViewModel = viewModel(), + onTocEntryClick: (TOCEntry) -> Unit, + onClose: () -> Unit, +) { + val readingUiState = viewModel.uiState.collectAsStateWithLifecycle() + val toc = readingUiState.value.toc ?: return + TableOfContentsPane(toc.toList(), onTocEntryClick, onClose) +} + +@Composable +fun TableOfContentsPane( + toc: List, + onTocEntryClick: (TOCEntry) -> Unit, + onClose: () -> Unit, +) { + SupportingPaneTemplate( + onClose, + paneTitle = stringResource(R.string.screen_toc) + ) { + Column( + Modifier.padding(horizontal = 16.dp) + ){ + LazyColumn { + items(toc) { entry -> + Text( + text = entry.title, + modifier = Modifier + .fillMaxWidth() + .clickable { onTocEntryClick(entry) } + .padding(vertical = 16.dp) + ) + } + } + } + } +} + +@Composable +fun TranslationPane( + onClose: () -> Unit, + viewModel: ReadingViewModel = viewModel(), +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + TranslationPane( + onClose, uiState.translationContent, uiState.info + ) +} + +@Composable +fun TranslationPane( + onClose: () -> Unit, + translationContent: String, + translationInfo: String, +) { + SupportingPaneTemplate( + onClose, + paneTitle = translationInfo + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.padding(top = 16.dp)) + Text(translationContent) + } + } +} + +@Composable +fun SupportingPaneTemplate( + onClose: () -> Unit, + paneTitle: String = "", + content: @Composable () -> Unit, +){ + Box(modifier = Modifier.fillMaxSize()) { + Card( + modifier = Modifier + .fillMaxSize(), + colors = CardColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.12f), + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) + ) { + Column( + Modifier.padding(horizontal = 16.dp) + ){ + Spacer(modifier = Modifier.padding(top = 30.dp)) + Text(paneTitle) + Spacer(modifier = Modifier.padding(top = 16.dp)) + HorizontalDivider( + thickness = 3.dp, + color = LatinReaderTheme.colorScheme.secondary + ) + content() + } + } + IconButton( + onClick = onClose, + modifier = Modifier.align(Alignment.TopEnd).padding(16.dp) + ) { + Icon( + Icons.Default.Close, + contentDescription = "Close", + modifier = Modifier.background(LatinReaderTheme.colorScheme.secondary) + ) + } + } +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/MarkdownScreen.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/MarkdownScreen.kt new file mode 100644 index 0000000..5005779 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/MarkdownScreen.kt @@ -0,0 +1,32 @@ +package com.telpirion.compose.ui.screens + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.ericmschmidt.classicsreader.ui.fragments.HelpFragment +import com.ericmschmidt.classicsreader.ui.fragments.InfoFragment +import com.mukesh.MarkDown +import com.telpirion.compose.ui.components.Screen + +/** + * Markdown screen for help and info. + */ +@Composable +fun MarkdownScreen( + modifier: Modifier = Modifier, + screen: Screen +) { + var textString: String + val context = LocalContext.current + textString = if (screen == Screen.Help) { + HelpFragment.buildHelpString(context) + } else { + InfoFragment.buildInfoString(context) + } + MarkDown( + modifier = modifier.padding(16.dp), + text = textString + ) +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/ReadingScreen.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/ReadingScreen.kt new file mode 100644 index 0000000..6398f89 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/ReadingScreen.kt @@ -0,0 +1,390 @@ +@file:Suppress("AssignedValueIsNeverRead") + +package com.telpirion.compose.ui.screens + +import android.app.Application +import android.content.Context +import android.util.Log +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior +import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold +import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.ericmschmidt.classicsreader.data.PreferencesDataStore +import com.ericmschmidt.classicsreader.data.PreferencesState +import com.telpirion.compose.MainActivity +import com.telpirion.compose.ui.components.Screen +import com.telpirion.compose.ui.components.TableOfContentsPane +import com.telpirion.compose.ui.components.TranslationPane +import com.telpirion.compose.viewmodels.DictionaryViewModel +import com.telpirion.compose.viewmodels.ReadingUiState +import com.telpirion.compose.viewmodels.ReadingViewModel +import kotlinx.coroutines.launch +import com.ericmschmidt.classicsreader.R as CoreResources + +private sealed class SupportingPaneContent { + object Hidden : SupportingPaneContent() + object Translation : SupportingPaneContent() + object TableOfContents : SupportingPaneContent() +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Suppress("unused") +@Composable +fun ReadingScreen( + navController: NavController, + workId: String? = "", + context: Context = LocalContext.current, + isTranslation: Boolean = false, + dictionaryViewModel: DictionaryViewModel = viewModel( + viewModelStoreOwner = (context as MainActivity) + ), + screen: Screen = Screen.Recent +) { + + val context = LocalContext.current + val preferencesDataStore = remember(context) { PreferencesDataStore(context) } + + val preferences = preferencesDataStore.preferencesFlow().collectAsState( + initial = PreferencesState() + ).value + + val recentlyRead = preferences.recentlyRead + val textSize = preferences.textSize + val poemLines = preferences.poemLines + val showPageControls = preferences.showPageControls + + var currentWorkId: String? = workId + if (screen == Screen.Recent) { + if (recentlyRead.isNotEmpty()) { + currentWorkId = recentlyRead + } + } + + val textSizeSp = textSize.toFloat() + var lineSpacing = 30.0f + + // If the font size is too big, then the text gets scrunched. + if (textSizeSp > 28.0) { + lineSpacing = 50.0f + } + + var uiState: ReadingUiState + var onPageTurn: (Boolean) -> Unit + var onPrev: () -> Unit + var onNext: () -> Unit + val viewModel: ReadingViewModel? + + Log.i("ReadingScreen", "screen: $screen") + if (screen == Screen.Vocab || screen == Screen.Dictionary){ + val dictionaryUiState = dictionaryViewModel.readingUiState.collectAsStateWithLifecycle() + uiState = dictionaryUiState.value + viewModel = null + onPageTurn = { + dictionaryViewModel.clearSearch() + } + onPrev = { + dictionaryViewModel.clearSearch() + } + onNext = { + dictionaryViewModel.clearSearch() + } + } else { + if (currentWorkId.isNullOrEmpty()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(stringResource(CoreResources.string.reading_no_book_open)) + } + return + } + + val readingViewModel: ReadingViewModel = viewModel( + factory = ReadingViewModel.Factory( + application = context.applicationContext as Application, + workId = currentWorkId, + isTranslation = isTranslation, + poemLines = poemLines, + preferencesDataStore = preferencesDataStore + ) + ) + viewModel = readingViewModel + val readingUiState by viewModel.uiState.collectAsStateWithLifecycle() + uiState = readingUiState + + onPageTurn = { + isNext -> viewModel.goToPage(isNext) + } + + onPrev = { + viewModel.goToPage(false) + } + + onNext = { + viewModel.goToPage(true) + } + } + + val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + var supportingPaneContent by remember { mutableStateOf(SupportingPaneContent.Hidden) } + val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange + + NavigableSupportingPaneScaffold( + navigator = scaffoldNavigator, + mainPane = { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = uiState.info, + style = MaterialTheme.typography.titleMedium + ) + + ReadingContent( + text = uiState.content, + textSizeSp = textSizeSp, + onPageTurn = onPageTurn, + onSwitchView = { + supportingPaneContent = SupportingPaneContent.Translation + scope.launch { + scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting) + } + }, + onShowToc = { + supportingPaneContent = SupportingPaneContent.TableOfContents + scope.launch { + scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting) + } + }, + modifier = Modifier.weight(1f), + lineHeight = lineSpacing + ) + + Text( + text = uiState.position, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + + if ((showPageControls) + && (screen != Screen.Vocab) + && (screen != Screen.Dictionary)) { + PageControls( + onPrev = onPrev, + onNext = onNext, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + }, + supportingPane = { + if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) { + when (supportingPaneContent) { + SupportingPaneContent.Translation -> { + if (currentWorkId != null) { + TranslationPane( + onClose = { + scope.launch { + supportingPaneContent = SupportingPaneContent.Hidden + scaffoldNavigator.navigateBack(backNavigationBehavior) + } + } + ) + } + } + + SupportingPaneContent.TableOfContents -> { + if (viewModel != null) { + TableOfContentsPane( + onTocEntryClick = { index -> + viewModel.goToChapter(index) + scope.launch { + supportingPaneContent = SupportingPaneContent.Hidden + scaffoldNavigator.navigateBack(backNavigationBehavior) + } + }, + onClose = { + scope.launch { + supportingPaneContent = SupportingPaneContent.Hidden + scaffoldNavigator.navigateBack(backNavigationBehavior) + } + } + ) + } + } + + SupportingPaneContent.Hidden -> { + // Empty pane + } + } + } + } + ) +} + +@Composable +private fun ReadingContent( + text: String, + textSizeSp: Float, + onPageTurn: (isNext: Boolean) -> Unit, + onSwitchView: () -> Unit, + onShowToc: () -> Unit, + modifier: Modifier = Modifier, + switchText: String = "Switch View", + lineHeight: Float = 1.2f, + + ) { + @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") + BoxWithConstraints( + modifier = modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + val viewWidth = maxWidth + val hitArea = viewWidth / 4 // Corresponds to HIT_AREA_RATIO = 4 + + var showContextMenu by remember { mutableStateOf(false) } + var contextMenuOffset by remember { mutableStateOf(DpOffset.Zero) } + + Text( + text = text, + fontSize = textSizeSp.sp, + lineHeight = lineHeight.sp, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .pointerInput(Unit) { + detectTapGestures( + onTap = { offset -> + when { + offset.x < hitArea.toPx() -> onPageTurn(false) + offset.x > (viewWidth - hitArea).toPx() -> onPageTurn(true) + else -> { + // Tapping in the middle shows the context menu + contextMenuOffset = DpOffset(offset.x.toDp(), offset.y.toDp()) + showContextMenu = true + } + } + } + ) + } + ) + + DropdownMenu( + expanded = showContextMenu, + onDismissRequest = { showContextMenu = false }, + offset = contextMenuOffset + ) { + DropdownMenuItem( + text = { Text(switchText) }, + onClick = { + onSwitchView() + showContextMenu = false + } + ) + DropdownMenuItem( + text = { Text("Table of Contents") }, + onClick = { + onShowToc() + showContextMenu = false + } + ) + } + } +} + + + + +@Composable +private fun PageControls( + onPrev: () -> Unit, + onNext: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + IconButton(onClick = onPrev, modifier = Modifier.weight(1f)) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(CoreResources.string.reading_btn_prev) + + ) + } + IconButton(onClick = onNext, modifier = Modifier.weight(1f)) { + Icon( + Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = stringResource(CoreResources.string.reading_btn_next) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun ReadingScreenPreview() { + MaterialTheme { + + val dictionaryViewModel: DictionaryViewModel = viewModel( + factory = DictionaryViewModel.Factory + ) + ReadingScreen( + workId = null, + navController = NavController(LocalContext.current), + dictionaryViewModel = dictionaryViewModel, + isTranslation = false + ) + } +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/SettingsScreen.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/SettingsScreen.kt new file mode 100644 index 0000000..43ec3a1 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/screens/SettingsScreen.kt @@ -0,0 +1,261 @@ +package com.telpirion.compose.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ericmschmidt.classicsreader.data.PreferencesDataStore +import com.ericmschmidt.classicsreader.data.PreferencesState +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import com.ericmschmidt.classicsreader.R as CoreResources + +/** + * The main composable for the settings screen, which displays a list of preferences. + * + * In a production app, the state for these settings would typically be hoisted to a + * ViewModel and persisted using DataStore. For this example, we use `rememberSavable` + * to maintain state across configuration changes. + */ +@Composable +fun SettingsScreen(modifier: Modifier = Modifier) { + val context = LocalContext.current + val preferencesDataStore = remember(context) { PreferencesDataStore(context) } + + val preferences = preferencesDataStore.preferencesFlow().collectAsState( + initial = PreferencesState() + ).value + + // Placeholder data for ListPreferences, as these are defined in XML arrays. + val textSizeOptions = stringArrayResource(CoreResources.array.pref_array) + val poemLinesOptions = stringArrayResource(CoreResources.array.poem_lines_array) + val displayTypeOptions = listOf( + stringResource(CoreResources.string.action_display_row), + "Grid" + ) + + val recentlyRead = preferences.recentlyRead + val textSize = preferences.textSize + val poemLines = preferences.poemLines + val showPageControls = preferences.showPageControls + val displayType = preferences.displayType + + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Text( + text = stringResource(CoreResources.string.action_settings), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 8.dp) + ) + } + + item { + SettingsListPreference( + title = stringResource(CoreResources.string.pref_text_size), + summary = stringResource(CoreResources.string.pref_text_size_summary), + currentValue = textSize.toString(), + options = textSizeOptions.toList(), + onValueChange = { + runBlocking { + launch { preferencesDataStore.updateTextSize(it.toInt()) } + } + } + ) + } + + item { + SettingsListPreference( + title = stringResource(CoreResources.string.pref_poem_lines), + summary = stringResource(CoreResources.string.pref_poem_lines_summary), + currentValue = poemLines.toString(), + options = poemLinesOptions.toList(), + onValueChange = { + runBlocking { + launch { preferencesDataStore.updatePoemLines(it.toInt()) } + } + } + ) + } + + item { + @Suppress("KotlinConstantConditions") + SettingsSwitchPreference( + title = stringResource(CoreResources.string.pref_show_page_controls), + summary = stringResource(CoreResources.string.pref_show_page_controls_summary), + isChecked = showPageControls, + onCheckedChange = { + runBlocking { + launch { preferencesDataStore.updateShowPageControls(it) } + } + } + ) + } + + item { + SettingsListPreference( + title = stringResource(CoreResources.string.action_display), + summary = stringResource(CoreResources.string.action_display_description), + currentValue = displayType, + options = displayTypeOptions, + onValueChange = { + runBlocking { + launch { preferencesDataStore.updateDisplayType(it) } + } + } + ) + } + } +} + +/** + * A composable that replicates the behavior of a ListPreference. + * + * @param title The title of the preference. + * @param summary A short description of the preference. + * @param currentValue The currently selected value. + * @param options The list of available options to choose from. + * @param onValueChange A callback invoked when a new value is selected. + */ +@Composable +private fun SettingsListPreference( + title: String, + summary: String, + currentValue: String, + options: List, + onValueChange: (String) -> Unit +) { + var showDialog by remember { mutableStateOf(false) } + + PreferenceItem( + title = title, + summary = summary, + onClick = { showDialog = true } + ) + + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { Text(title) }, + text = { + Column { + options.forEach { option -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onValueChange(option) + showDialog = false + }, + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (option == currentValue), + onClick = { + onValueChange(option) + showDialog = false + } + ) + Text( + text = option, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = { showDialog = false }) { + Text("Cancel") + } + } + ) + } +} + +/** + * A composable that replicates the behavior of a CheckBoxPreference or SwitchPreference. + * + * @param title The title of the preference. + * @param summary A short description of the preference. + * @param isChecked The current checked state. + * @param onCheckedChange A callback invoked when the state is changed. + */ +@Composable +private fun SettingsSwitchPreference( + title: String, + summary: String, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onCheckedChange(!isChecked) } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text(text = summary, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange, + modifier = Modifier.padding(start = 16.dp) + ) + } +} + +@Composable +private fun PreferenceItem( + title: String, + summary: String, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 8.dp) + ) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text(text = summary, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} + +@Preview(showBackground = true) +@Composable +fun SettingsScreenPreview() { + MaterialTheme { + SettingsScreen() + } +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Color.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Color.kt index 287e0c7..ae4c208 100644 --- a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Color.kt +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Color.kt @@ -2,10 +2,22 @@ package com.telpirion.compose.ui.theme import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) +val TelpirionWhite = Color(0xFFFFFFFF) +val TelpirionOrange = Color(0xFFF46A6B) +val TelpirionGray = Color(0xFF3E444A) + +val TWhite40 = Color(0xFFF0F1F2) +val TWhite80 = Color(0xFFF2F3F2) + +val TOrange40 = Color(0xFFF46A6B) +val TOrange80 = Color(0xFFF46A6B) + +val TGray40 = Color(0xFF7E878E) +val TGray80 = Color(0xFF191919) + + + +val DebugRed = Color(0xFFF44336) +val DebugGreen = Color(0xFF4CAF50) + -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Theme.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Theme.kt index 90d7934..4eee588 100644 --- a/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Theme.kt +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/ui/theme/Theme.kt @@ -1,50 +1,32 @@ package com.telpirion.compose.ui.theme -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + primary = TelpirionWhite, + secondary = TelpirionOrange, + tertiary = TelpirionGray, + background = TGray40, + onBackground = TelpirionWhite, + onPrimary = TGray80, + onSecondary = TWhite40, + onTertiary = TOrange40, + onSurface = TGray40, ) +private val DarkColorScheme = lightColorScheme().copy() + @Composable -fun LatinReaderTheme( +fun ReaderTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme else -> LightColorScheme } @@ -54,4 +36,10 @@ fun LatinReaderTheme( typography = Typography, content = content ) +} + +object LatinReaderTheme { + val colorScheme: ColorScheme + @Composable + get() = MaterialTheme.colorScheme } \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/viewmodels/DictionaryViewModel.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/viewmodels/DictionaryViewModel.kt new file mode 100644 index 0000000..3d826a2 --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/viewmodels/DictionaryViewModel.kt @@ -0,0 +1,148 @@ +package com.telpirion.compose.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.ericmschmidt.classicsreader.data.DictionaryRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.random.Random + +// Represents the state of the dictionary UI +data class DictionaryUiState( + val searchQuery: String = "", + val searchResult: String? = null, + val searchHistory: List = emptyList(), + val isResultVisible: Boolean = false +) + +class DictionaryViewModel(application: Application) : AndroidViewModel(application) { + + private val dictionaryRepository = DictionaryRepository(application) + + private val _uiState = MutableStateFlow(DictionaryUiState()) + val uiState = _uiState.asStateFlow() + + // We use a second UIState field to expose values to the ReadingScreen. + private val _readingUiState = MutableStateFlow(ReadingUiState()) + val readingUiState = _readingUiState.asStateFlow() + + + init { + // Observe the search history from the repository + viewModelScope.launch { + dictionaryRepository.searchHistoryFlow.collect { history -> + _uiState.update { it.copy(searchHistory = history) } + if (history.isNotEmpty()){ + _readingUiState.update {it.copy( + info = "", + content = "", + position = "", + )} + } + } + } + } + + /** Updates the search query in the UI state. */ + fun onQueryChange(query: String) { + _uiState.update { + currentState -> currentState.copy(searchQuery = query) + } + _readingUiState.update { + currentState -> currentState.copy( + info = query, + content = "", + ) + } + } + + /** Performs a search for the given query. */ + fun search(query: String) { + val trimmedQuery = query.trim() + if (trimmedQuery.isBlank()) return + + viewModelScope.launch { + // Get definition from the repository + val definition = dictionaryRepository.getDefinition(trimmedQuery) + + if (!definition.isNullOrEmpty()) { + dictionaryRepository.addSearchTerm(trimmedQuery) + } + + _uiState.update { + it.copy( + searchResult = definition ?: "Entry not found.", + isResultVisible = true + ) + } + _readingUiState.update { + it.copy( + content = definition ?: "Entry not found", + info = trimmedQuery, + position = dictionaryRepository.getDictionaryInfo() + ) + } + } + } + + /** Clears the search query text. */ + fun clearSearch() { + _uiState.update { it.copy(searchQuery = "") } + _readingUiState.update { it.copy( + content = "", + info = "", + position = "", + ) } + + } + + /** Hides the search result dialog. */ + @Suppress("unused") + fun dismissResult() { + _uiState.update { it.copy(isResultVisible = false, searchResult = null) } + _readingUiState.update { it.copy( + content = "", + info = "", + position = "", + ) } + } + + fun getVocab() { + // Use a randomly generated term as a fallback if there is no search history to pull from + var vocabTerm = dictionaryRepository.getRandom() + val vocabList = uiState.value.searchHistory.toMutableList() + val vocabTermSize = vocabList.size + if (vocabTermSize > 0) { + val random = Random.Default + vocabTerm = vocabList[random.nextInt(until = vocabTermSize)] + } + viewModelScope.launch { + val definition = dictionaryRepository.getDefinition(vocabTerm) + _readingUiState.update { + + it.copy( + content = definition ?: "Entry not found", + info = vocabTerm, + position = dictionaryRepository.getDictionaryInfo() + ) + } + } + } + + // A companion object for the factory is a common pattern for easy access. + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = (this[APPLICATION_KEY] as Application) + DictionaryViewModel(application) + } + } + } +} \ No newline at end of file diff --git a/LatinReader/compose/src/main/java/com/telpirion/compose/viewmodels/ReadingViewModel.kt b/LatinReader/compose/src/main/java/com/telpirion/compose/viewmodels/ReadingViewModel.kt new file mode 100644 index 0000000..855933a --- /dev/null +++ b/LatinReader/compose/src/main/java/com/telpirion/compose/viewmodels/ReadingViewModel.kt @@ -0,0 +1,148 @@ +package com.telpirion.compose.viewmodels + +import android.app.Application +import android.content.Context +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.datastore.dataStore +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.ericmschmidt.classicsreader.MyApplication +import com.ericmschmidt.classicsreader.data.PreferencesDataStore +import com.ericmschmidt.classicsreader.data.PreferencesState +import com.ericmschmidt.classicsreader.data.TOCEntry +import com.ericmschmidt.classicsreader.data.WorkInfo +import com.ericmschmidt.classicsreader.data.RECENTLY_READ +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import com.ericmschmidt.classicsreader.R as CoreResources +import com.ericmschmidt.classicsreader.data.ReadingViewModel as RVM + +data class ReadingUiState( + val content: String = "", + val translationContent: String = "", + val info: String = "", + val position: String = "", + val tocAvailable: Boolean = false, + val isTranslation: Boolean = false, + val toc: Array? = emptyList().toTypedArray(), +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ReadingUiState + + if (tocAvailable != other.tocAvailable) return false + if (isTranslation != other.isTranslation) return false + if (content != other.content) return false + if (info != other.info) return false + if (position != other.position) return false + if (!toc.contentEquals(other.toc)) return false + + return true + } + + override fun hashCode(): Int { + var result = tocAvailable.hashCode() + result = 31 * result + isTranslation.hashCode() + result = 31 * result + content.hashCode() + result = 31 * result + info.hashCode() + result = 31 * result + position.hashCode() + result = 31 * result + (toc?.contentHashCode() ?: 0) + return result + } +} + +class ReadingViewModel( + application: Application, + workId: String?, + private val isTranslation: Boolean, + private val poemLines: Int, + private val preferencesDataStore: PreferencesDataStore, +) : ViewModel() { + + private val _uiState = MutableStateFlow(ReadingUiState()) + val uiState: StateFlow = _uiState + + private var workInfo: WorkInfo? = null + private var contentLines: List = emptyList() + + private var translationContentLines: List = emptyList() + + private var content: RVM? = null + private var translationContent: RVM? = null + + init { + if (!workId.isNullOrEmpty()) { + val application = MyApplication.applicationInstance() + val library = application.library + workInfo = library.getWorkInfoByID(workId) + + // TODO(telpirion): integrate old ReaderViewModel with new one + content = RVM(workInfo as WorkInfo, isTranslation, poemLines) + translationContent = RVM(workInfo as WorkInfo, !isTranslation, poemLines) + + @Suppress("UNCHECKED_CAST") + contentLines = listOf(content?.getCurrentPage()) as List<*> as List + @Suppress("UNCHECKED_CAST") + translationContentLines = listOf(translationContent?.getCurrentPage()) as List<*> as List + + updateState() + + // Update the recently read + runBlocking { + launch { + preferencesDataStore.updateRecentlyRead(workId) + } + } + } else { + _uiState.value = ReadingUiState( + content = application.getString(CoreResources.string.reading_no_book_open) + ) + } + } + + fun goToPage(isNext: Boolean) { + this.content?.goToPage(isNext) + updateState() + } + + fun goToChapter(entry: TOCEntry){ + val book = entry.book + val page = entry.line + this.content?.setCurrentBook(book) + this.content?.setCurrentLine(page) + updateState() + } + + @Suppress("UNCHECKED_CAST") + private fun updateState() { + contentLines = listOf(content?.getCurrentPage()) as List<*> as List + translationContentLines = listOf(translationContent?.getCurrentPage()) as List<*> as List + _uiState.value = ReadingUiState( + content = contentLines.joinToString("\n"), + translationContent = translationContentLines.joinToString("\n"), + info = workInfo?.title ?: "Unknown Work", + position = content?.getReadingPositionString() as String, + tocAvailable = workInfo?.tocEntries?.isNotEmpty() ?: false, + isTranslation = isTranslation, + toc = workInfo?.tocEntries?.toTypedArray()) + } + + @Suppress("UNCHECKED_CAST") + class Factory( + private val application: Application, + private val workId: String?, + private val isTranslation: Boolean, + private val preferencesDataStore: PreferencesDataStore, + private val poemLines: Int + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return ReadingViewModel(application, workId, isTranslation, poemLines, preferencesDataStore) as T + } + } +} diff --git a/LatinReader/compose/src/main/res/values/config.xml b/LatinReader/compose/src/main/res/values/config.xml new file mode 100644 index 0000000..89ddb09 --- /dev/null +++ b/LatinReader/compose/src/main/res/values/config.xml @@ -0,0 +1,6 @@ + + + com.ericmschmidt.classicsreader.data.placeholders.PseudoManifest + false + error + \ No newline at end of file diff --git a/LatinReader/compose/src/main/res/values/strings.xml b/LatinReader/compose/src/main/res/values/strings.xml index a574dc7..1d1e099 100644 --- a/LatinReader/compose/src/main/res/values/strings.xml +++ b/LatinReader/compose/src/main/res/values/strings.xml @@ -1,3 +1,15 @@ - compose + Reader + Library + Recent + Settings + Open navigation drawer + Search dictionary + Open search + Close search + Clear search + Translations + Vocabulary + Table of Contents + \ No newline at end of file diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/DictionaryRepository.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/DictionaryRepository.kt new file mode 100644 index 0000000..7cf4cab --- /dev/null +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/DictionaryRepository.kt @@ -0,0 +1,76 @@ +package com.ericmschmidt.classicsreader.data + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringSetPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.io.IOException + +// Define the DataStore instance once in a central file. +val Context.dataStore: DataStore by preferencesDataStore(name = "dictionary_prefs") + +/** + * Repository to manage dictionary data, including search history and definitions. + */ +class DictionaryRepository(context: Context) { + + private val dictionary = Dictionary() // Legacy Java dictionary class + private val dataStore = context.dataStore + + private object PreferencesKeys { + val SEARCH_HISTORY = stringSetPreferencesKey("search_history") + } + + /** + * A flow that emits the user's search history. + */ + val searchHistoryFlow: Flow> = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { preferences -> + (preferences[PreferencesKeys.SEARCH_HISTORY] ?: emptySet()).sorted() + } + + /** + * Adds a new term to the search history in DataStore. + */ + suspend fun addSearchTerm(term: String) { + dataStore.edit { preferences -> + val currentHistory = preferences[PreferencesKeys.SEARCH_HISTORY] ?: emptySet() + // Add the new term, ensuring it's clean and lowercase. + preferences[PreferencesKeys.SEARCH_HISTORY] = currentHistory + term.trim().lowercase() + } + } + + /** + * Retrieves a definition using the legacy Dictionary class. + * This is a suspending function to allow moving the work off the main thread. + */ + suspend fun getDefinition(entry: String): String? = withContext(Dispatchers.IO) { + dictionary.getEntry(entry) + } + + /** + * Retrieves the dictionary's title. + */ + fun getDictionaryInfo(): String { + return dictionary.dictionaryInfo?.title as String + } + + fun getRandom(): String { + return dictionary.getRandomEntry() as String + } +} \ No newline at end of file diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/Library.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/Library.kt index 2ecc751..a218060 100644 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/Library.kt +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/Library.kt @@ -1,5 +1,6 @@ package com.ericmschmidt.classicsreader.data +import androidx.compose.runtime.Composable import com.ericmschmidt.classicsreader.logError import java.util.ArrayList @@ -39,6 +40,9 @@ open class Library { return collection?.toTypedArray() ?: emptyArray() } + @Composable + open fun GetHeaderIcon() {} + companion object { /** * Get the library class out of the package using reflection. diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/PreferencesDataStore.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/PreferencesDataStore.kt new file mode 100644 index 0000000..946be8c --- /dev/null +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/PreferencesDataStore.kt @@ -0,0 +1,78 @@ +package com.ericmschmidt.classicsreader.data + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@Suppress("KotlinConstantConditions") +data class PreferencesState( + val textSize: Int = SettingsFields.TEXT_SIZE_DEFAULT.toInt(), + val poemLines: Int = SettingsFields.POEM_LINES_DEFAULT.toInt(), + val showPageControls: Boolean = SettingsFields.SHOW_PAGE_CONTROLS_DEFAULT, + val displayType: String = SettingsFields.DISPLAY_TYPE_DEFAULT, + val recentlyRead: String = SettingsFields.RECENTLY_READ, +) + +class PreferencesDataStore(val context: Context) { + private object PreferencesKeys { + val TEXT_SIZE = intPreferencesKey(SettingsFields.TEXT_SIZE) + val POEM_LINES = intPreferencesKey(SettingsFields.POEM_LINES) + val SHOW_PAGE_CONTROLS = booleanPreferencesKey(SettingsFields.SHOW_PAGE_CONTROLS) + val DISPLAY_TYPE = stringPreferencesKey(SettingsFields.DISPLAY_TYPE) + val RECENTLY_READ = stringPreferencesKey(SettingsFields.RECENTLY_READ) + } + + fun preferencesFlow(): Flow = context.dataStore.data.map { preferences -> + @Suppress("KotlinConstantConditions") + return@map PreferencesState( + textSize = preferences[PreferencesKeys.TEXT_SIZE] ?: SettingsFields.TEXT_SIZE_DEFAULT.toInt(), + poemLines = preferences[PreferencesKeys.POEM_LINES] ?: SettingsFields.POEM_LINES_DEFAULT.toInt(), + showPageControls = preferences[PreferencesKeys.SHOW_PAGE_CONTROLS] ?: SettingsFields.SHOW_PAGE_CONTROLS_DEFAULT, + displayType = preferences[PreferencesKeys.DISPLAY_TYPE] ?: SettingsFields.DISPLAY_TYPE_DEFAULT, + recentlyRead = preferences[PreferencesKeys.RECENTLY_READ] ?: "" + ) + } + + suspend fun updateTextSize(newValue: Int) { + writeIntSetting(context, PreferencesKeys.TEXT_SIZE, newValue) + } + + suspend fun updatePoemLines(newValue: Int) { + writeIntSetting(context, PreferencesKeys.POEM_LINES, newValue) + } + + suspend fun updateShowPageControls(newValue: Boolean) { + writeBoolSetting(context, PreferencesKeys.SHOW_PAGE_CONTROLS, newValue) + } + + suspend fun updateDisplayType(newValue: String) { + writeStringSetting(context, PreferencesKeys.DISPLAY_TYPE, newValue) + } + + suspend fun updateRecentlyRead(newValue: String) { + writeStringSetting(context, PreferencesKeys.RECENTLY_READ, newValue) + } + + private suspend fun writeStringSetting(context: Context, key: Preferences.Key, newValue: String) { + context.dataStore.edit { settings -> + settings[key] = newValue + } + } + + private suspend fun writeIntSetting(context: Context, key: Preferences.Key, newValue: Int) { + context.dataStore.edit { settings -> + settings[key] = newValue + } + } + + private suspend fun writeBoolSetting(context: Context, key: Preferences.Key, newValue: Boolean) { + context.dataStore.edit { settings -> + settings[key] = newValue + } + } +} \ No newline at end of file diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/ReadingViewModel.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/ReadingViewModel.kt index 1f984ff..f0b5c5f 100644 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/ReadingViewModel.kt +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/ReadingViewModel.kt @@ -51,12 +51,12 @@ class ReadingViewModel( if (isTranslation) { currentWork = Work(workInfo.englishLocation) - author = workInfo.englishAuthor - title = workInfo.englishTitle + author = workInfo.englishAuthor as String + title = workInfo.englishTitle as String } else { currentWork = Work(workInfo.location) - author = workInfo.author - title = workInfo.title + author = workInfo.author as String + title = workInfo.title as String } currentBook = currentWork.getBook(currentBookIndex) diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/SettingsFields.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/SettingsFields.kt new file mode 100644 index 0000000..61b8b3f --- /dev/null +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/SettingsFields.kt @@ -0,0 +1,23 @@ +package com.ericmschmidt.classicsreader.data + +object SettingsFields { + const val RECENTLY_READ = "recently_read" + const val TEXT_SIZE = "textSize" + const val TEXT_SIZE_DEFAULT = "20" + const val POEM_LINES = "poemLines" + const val POEM_LINES_DEFAULT = "5" + const val SHOW_PAGE_CONTROLS = "showPageControls" + const val SHOW_PAGE_CONTROLS_DEFAULT = true + const val DISPLAY_TYPE = "displayType" + const val DISPLAY_TYPE_DEFAULT = "List" +} + +const val RECENTLY_READ = "recently_read" +const val TEXT_SIZE = "textSize" +const val TEXT_SIZE_DEFAULT = "20" +const val POEM_LINES = "poemLines" +const val POEM_LINES_DEFAULT = "5" +const val SHOW_PAGE_CONTROLS = "showPageControls" +const val SHOW_PAGE_CONTROLS_DEFAULT = true +const val DISPLAY_TYPE = "displayType" +const val DISPLAY_TYPE_DEFAULT = "List" \ No newline at end of file diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/activities/MainActivity.java b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/placeholders/PseudoManifest.kt similarity index 100% rename from LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/activities/MainActivity.java rename to LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/data/placeholders/PseudoManifest.kt diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/components/Cards.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/components/Cards.kt index 569835d..fa506cf 100644 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/components/Cards.kt +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/components/Cards.kt @@ -71,7 +71,7 @@ fun PrettyCard( textAlign = TextAlign.Left ) Text( - text = if (!isTranslation) workInfo.author else workInfo.englishAuthor, + text = if (!isTranslation) workInfo.author else workInfo.englishAuthor, textAlign = TextAlign.Left ) } diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/InfoFragment.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/InfoFragment.kt index 6773792..4de8c68 100644 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/InfoFragment.kt +++ b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/InfoFragment.kt @@ -1,5 +1,6 @@ package com.ericmschmidt.classicsreader.ui.fragments +import android.content.Context import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -23,14 +24,17 @@ class InfoFragment : Fragment() { return inflater.inflate(R.layout.fragment_info, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val markdownView = view.findViewById(R.id.info_markdown_view) + val infoString = buildInfoString(requireContext()) + markdownView.setMarkDownText(infoString) + } - val markdownView = view.findViewById(R.id.info_markdown_view) - val applicationInstance = MyApplication.Factory.applicationInstance() + companion object InfoStringBuilder { + fun buildInfoString(context: Context): String { // Get the app context - val context = applicationInstance.context val resources = context.resources val packageManager = context.packageManager val packageName = context.packageName @@ -49,7 +53,8 @@ class InfoFragment : Fragment() { val versionNumber = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { versionInfo.longVersionCode } else { - versionInfo.versionCode + @Suppress("DEPRECATION") + versionInfo.versionCode } // Build the Markdown-formatted information screen @@ -59,6 +64,7 @@ class InfoFragment : Fragment() { infoString = infoString.replace("{{versionName}}", versionName.toString()) infoString = infoString.replace("{{versionCode}}", versionNumber.toString()) - markdownView.setMarkDownText(infoString) - } + return infoString + } + } } \ No newline at end of file diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Color.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Color.kt deleted file mode 100644 index 33ca0ba..0000000 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Color.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.ericmschmidt.classicsreader.ui.theme - -import androidx.compose.ui.graphics.Color - -val md_theme_light_primary = Color(0xFF006879) -val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFFA9EDFF) -val md_theme_light_onPrimaryContainer = Color(0xFF001F26) -val md_theme_light_secondary = Color(0xFF4B6268) -val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFCEE7EE) -val md_theme_light_onSecondaryContainer = Color(0xFF061F24) -val md_theme_light_tertiary = Color(0xFF565D7E) -val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFDDE1FF) -val md_theme_light_onTertiaryContainer = Color(0xFF121A37) -val md_theme_light_error = Color(0xFFBA1A1A) -val md_theme_light_errorContainer = Color(0xFFFFDAD6) -val md_theme_light_onError = Color(0xFFFFFFFF) -val md_theme_light_onErrorContainer = Color(0xFF410002) -val md_theme_light_background = Color(0xFFFBFCFD) -val md_theme_light_onBackground = Color(0xFF191C1D) -val md_theme_light_surface = Color(0xFFFBFCFD) -val md_theme_light_onSurface = Color(0xFF191C1D) -val md_theme_light_surfaceVariant = Color(0xFFDBE4E7) -val md_theme_light_onSurfaceVariant = Color(0xFF3F484B) -val md_theme_light_outline = Color(0xFF6F797B) -val md_theme_light_inverseOnSurface = Color(0xFFEFF1F2) -val md_theme_light_inverseSurface = Color(0xFF2E3132) -val md_theme_light_inversePrimary = Color(0xFF54D7F3) -val md_theme_light_surfaceTint = Color(0xFF006879) -val md_theme_light_outlineVariant = Color(0xFFBFC8CB) -val md_theme_light_scrim = Color(0xFF000000) - -val md_theme_dark_primary = Color(0xFF54D7F3) -val md_theme_dark_onPrimary = Color(0xFF003640) -val md_theme_dark_primaryContainer = Color(0xFF004E5B) -val md_theme_dark_onPrimaryContainer = Color(0xFFA9EDFF) -val md_theme_dark_secondary = Color(0xFFB2CBD2) -val md_theme_dark_onSecondary = Color(0xFF1C343A) -val md_theme_dark_secondaryContainer = Color(0xFF334A50) -val md_theme_dark_onSecondaryContainer = Color(0xFFCEE7EE) -val md_theme_dark_tertiary = Color(0xFFBEC5EB) -val md_theme_dark_onTertiary = Color(0xFF282F4D) -val md_theme_dark_tertiaryContainer = Color(0xFF3E4565) -val md_theme_dark_onTertiaryContainer = Color(0xFFDDE1FF) -val md_theme_dark_error = Color(0xFFFFB4AB) -val md_theme_dark_errorContainer = Color(0xFF93000A) -val md_theme_dark_onError = Color(0xFF690005) -val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -val md_theme_dark_background = Color(0xFF191C1D) -val md_theme_dark_onBackground = Color(0xFFE1E3E4) -val md_theme_dark_surface = Color(0xFF191C1D) -val md_theme_dark_onSurface = Color(0xFFE1E3E4) -val md_theme_dark_surfaceVariant = Color(0xFF3F484B) -val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB) -val md_theme_dark_outline = Color(0xFF899295) -val md_theme_dark_inverseOnSurface = Color(0xFF191C1D) -val md_theme_dark_inverseSurface = Color(0xFFE1E3E4) -val md_theme_dark_inversePrimary = Color(0xFF006879) -val md_theme_dark_surfaceTint = Color(0xFF54D7F3) -val md_theme_dark_outlineVariant = Color(0xFF3F484B) -val md_theme_dark_scrim = Color(0xFF000000) diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Theme.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Theme.kt deleted file mode 100644 index d23cb74..0000000 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Theme.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.ericmschmidt.classicsreader.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat - -private val LightColorScheme = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim, -) - -private val DarkColorScheme = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim, -) - -/* -@Composable -fun ReaderTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = false, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme - } - } - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) - -} - */ \ No newline at end of file diff --git a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Type.kt b/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Type.kt deleted file mode 100644 index 9d1dd52..0000000 --- a/LatinReader/core/src/main/java/com/ericmschmidt/classicsreader/ui/theme/Type.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.ericmschmidt.classicsreader.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) -) diff --git a/LatinReader/core/src/main/res/values/strings.xml b/LatinReader/core/src/main/res/values/strings.xml index 0fca3e6..4f7dbef 100755 --- a/LatinReader/core/src/main/res/values/strings.xml +++ b/LatinReader/core/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + Latin Reader telpirion.com @@ -48,7 +48,7 @@ Error I\'m really sorry - there was an error Something bad happened and the app needs to close. The information below shows the gnarly details. - No details to show. + No details to show. Show translation Show source text @@ -56,6 +56,5 @@ Contents: Library display - List - Switch the display of works in the library between list and grid. + ListSwitch the display of works in the library between list and grid. diff --git a/LatinReader/core/src/main/res/values/styles.xml b/LatinReader/core/src/main/res/values/styles.xml index 2ea7b0d..a8aa669 100755 --- a/LatinReader/core/src/main/res/values/styles.xml +++ b/LatinReader/core/src/main/res/values/styles.xml @@ -20,7 +20,7 @@ diff --git a/LatinReader/greekreader-compose/.gitignore b/LatinReader/greekreader-compose/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/LatinReader/greekreader-compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/LatinReader/greekreader-compose/build.gradle b/LatinReader/greekreader-compose/build.gradle new file mode 100644 index 0000000..3ab88fd --- /dev/null +++ b/LatinReader/greekreader-compose/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.ericmschmidt.classicsreader.greek.compose' + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId "com.ericmschmidt.classicsreader.greek.compose" + minSdk 24 + targetSdk 36 + versionCode(3) + versionName("2.1.0") + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + kotlin { + jvmToolchain { + languageVersion = JavaLanguageVersion.of("24") + } + } +} + +dependencies { + implementation project(":core") + implementation project(":compose") + implementation project(":greekreader") + implementation 'androidx.core:core-ktx:1.17.0' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.13.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' +} \ No newline at end of file diff --git a/LatinReader/greekreader-compose/proguard-rules.pro b/LatinReader/greekreader-compose/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/LatinReader/greekreader-compose/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/LatinReader/greekreader-compose/src/androidTest/java/com/ericmschmidt/classicsreader/greek/compose/ExampleInstrumentedTest.kt b/LatinReader/greekreader-compose/src/androidTest/java/com/ericmschmidt/classicsreader/greek/compose/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d4db1d9 --- /dev/null +++ b/LatinReader/greekreader-compose/src/androidTest/java/com/ericmschmidt/classicsreader/greek/compose/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.ericmschmidt.classicsreader.greek.compose + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.ericmschmidt.classicsreader.greek.compose", appContext.packageName) + } +} \ No newline at end of file diff --git a/LatinReader/greekreader-compose/src/main/AndroidManifest.xml b/LatinReader/greekreader-compose/src/main/AndroidManifest.xml new file mode 100644 index 0000000..99d46b6 --- /dev/null +++ b/LatinReader/greekreader-compose/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LatinReader/greekreader-compose/src/test/java/com/ericmschmidt/classicsreader/greek/compose/ExampleUnitTest.kt b/LatinReader/greekreader-compose/src/test/java/com/ericmschmidt/classicsreader/greek/compose/ExampleUnitTest.kt new file mode 100644 index 0000000..f7d7f4a --- /dev/null +++ b/LatinReader/greekreader-compose/src/test/java/com/ericmschmidt/classicsreader/greek/compose/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.ericmschmidt.classicsreader.greek.compose + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibrary.kt b/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibrary.kt index a0ddd43..cbe5f50 100644 --- a/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibrary.kt +++ b/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibrary.kt @@ -3,6 +3,7 @@ package com.ericmschmidt.greekreader.data import com.ericmschmidt.classicsreader.data.Library +import com.ericmschmidt.classicsreader.data.TOCEntry import com.ericmschmidt.classicsreader.data.WorkInfo import com.ericmschmidt.greekreader.R @@ -24,77 +25,79 @@ class GreekReaderLibrary: Library() { id = "AristotlePol", title = "Politics", author = "Aristotle", + editor = "W. D. Ross", + translator = "H. Rackham", englishTitle = "Politics", englishAuthor = "Aristotle", location = R.raw.gk_aristot_pol_gk, englishLocation = R.raw.aristot_pol_eng, workType = WorkInfo.WorkType.PROSE, - image = R.drawable.work_politics), + image = R.drawable.work_politics, + description = aristotlePoliticsDescription, + tocEntries = arrayListOf( + TOCEntry("Book 1", 0, 0), + TOCEntry("Book 2", 1, 0), + TOCEntry("Book 3", 2, 0), + TOCEntry("Book 4", 3, 0), + TOCEntry("Book 5", 4, 0), + TOCEntry("Book 6", 5, 0), + TOCEntry("Book 7", 6, 0), + TOCEntry("Book 8", 7, 0) + )), WorkInfo( id = "HomerIliad", author = "Homer", title = "Iliad", + translator = "A.T. Murray, Ph.D.", + editor = "Thomas W. Allen", englishAuthor = "Homer", englishTitle = "Iliad", location = R.raw.gk_hom_il_gk, englishLocation = R.raw.hom_il_eng, workType = WorkInfo.WorkType.POEM, offset = 1, - image = R.drawable.work_iliad), + image = R.drawable.work_iliad, + description = homerIliadDescription), WorkInfo( id = "HomerOdyssey", author = "Homer", title = "Odyssey", + editor = "A.T. Murray", + translator = "A.T. Murray, Ph.D.", englishAuthor = "Homer", englishTitle = "Odyssey", location = R.raw.gk_hom_od_gk, englishLocation = R.raw.hom_od_eng, workType = WorkInfo.WorkType.POEM, offset = 1, - image = R.drawable.work_odyssey), + image = R.drawable.work_odyssey, + description = homerOdysseyDescription), WorkInfo( id = "XenophonAn", author = "Xenophon", title = "Anabasis", + editor = "E. C. Marchant", + translator = "Carleton L. Brownson", englishAuthor = "Xenophon", englishTitle = "Anabasis", location = R.raw.gk_xen_anab_gk, englishLocation = R.raw.xen_anab_eng, workType = WorkInfo.WorkType.PROSE, - image = R.drawable.work_anabasis), + image = R.drawable.work_anabasis, + description = xenophonAnabasisDescription), WorkInfo( id = "Lysias", author = "Lysias", title = "Speeches", + editor = "W.R.M. Lamb, M.A.", + translator = "W.R.M. Lamb, M.A.", englishTitle = "Speeches", englishAuthor = "Lysias", location = R.raw.gk_lys_gk, englishLocation = R.raw.lys_eng, workType = WorkInfo.WorkType.PROSE, - image = R.drawable.work_speeches), - // TODO: Fix Herodotus transcription. - // "Histories" is also a title that breaks the list ... - WorkInfo( - id = "Herodotus", - title = "Mysteries", - author = "Herodotus", - englishTitle = "Histories", - englishAuthor = "Herodotus", - location = R.raw.gk_hdt_gk, - englishLocation = R.raw.hdt_eng, - workType = WorkInfo.WorkType.PROSE), - - // TODO: Fix Plato's Republic Transcription - // "Republic" is also a title that breaks the list ... - WorkInfo( - id = "PlatoRep", - title = "Republic", - author = "Plato", - englishTitle = "Republic", - englishAuthor = "Plato", - location = R.raw.gk_plat_rep_gk, - englishLocation = R.raw.plat_rep_eng, - workType = WorkInfo.WorkType.PROSE) + image = R.drawable.work_speeches, + description = lysiasDescription), ) @@ -105,7 +108,8 @@ class GreekReaderLibrary: Library() { englishAuthor = "Henry George Liddell and Robert Scott", englishTitle = "An Intermediate Greek-English Lexicon", location = R.raw.ml, - englishLocation = R.raw.ml) + englishLocation = R.raw.ml, + description = dictionaryDescription) /** * Gets the resource ID of the dictionary entry file. diff --git a/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibraryDescriptions.kt b/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibraryDescriptions.kt new file mode 100644 index 0000000..b36129e --- /dev/null +++ b/LatinReader/greekreader/src/main/java/com/ericmschmidt/greekreader/data/GreekReaderLibraryDescriptions.kt @@ -0,0 +1,13 @@ +package com.ericmschmidt.greekreader.data + +const val aristotlePoliticsDescription = "A foundational work of political philosophy, Aristotle's 'Politics' examines the city-state's purpose, constitutions, and the ideal political community. It explores justice, citizenship, and the nature of government." + +const val homerIliadDescription = "An epic poem recounting the final year of the Trojan War. The 'Iliad' focuses on the rage of Achilles and the devastating consequences of pride and conflict for gods and mortals." + +const val homerOdysseyDescription = "This epic poem follows the Greek hero Odysseus on his ten-year journey home from the Trojan War. He faces mythical creatures and divine wrath in his quest to return to Ithaca." + +const val xenophonAnabasisDescription = "Xenophon's 'Anabasis' is a historical account of the Ten Thousand, Greek mercenaries who marched deep into the Persian Empire and fought their way back to Greece after their leader was killed." + +const val lysiasDescription = "A collection of speeches from the Athenian orator Lysias. These legal arguments provide insight into Athenian law, society, and daily life in the 4th century BC, showcasing masterful rhetoric." + +const val dictionaryDescription = "An Intermediate Greek-English Lexicon, founded upon the seventh edition of Liddell and Scott's Greek-English Lexicon. A key resource for students of ancient Greek literature and philosophy." diff --git a/LatinReader/greekreader/src/main/res/drawable/ic_greekreader.xml b/LatinReader/greekreader/src/main/res/drawable/ic_greekreader.xml new file mode 100644 index 0000000..fc03baf --- /dev/null +++ b/LatinReader/greekreader/src/main/res/drawable/ic_greekreader.xml @@ -0,0 +1,11 @@ + + + diff --git a/LatinReader/greekreader/src/main/res/raw/hdt_gk.xml b/LatinReader/greekreader/src/main/res/raw/hdt_gk.xml index 8a0f93f..be87091 100644 --- a/LatinReader/greekreader/src/main/res/raw/hdt_gk.xml +++ b/LatinReader/greekreader/src/main/res/raw/hdt_gk.xml @@ -32,7 +32,8 @@ -

+ +

*(hrodo/tou *(alikarnhsse/os i(stori/hs a)po/decis h(/de, w(s mh/te ta\ geno/mena e)c a)nqrw/pwn tw=| xro/nw| e)ci/thla ge/nhtai, mh/te e)/rga mega/la te kai\ qwmasta/, ta\ me\n *(/ellhsi ta\ de\ barba/roisi a)podexqe/nta, a)klea= ge/nhtai, ta/ te a)/lla kai\ di' h(\n ai)ti/hn e)pole/mhsan a)llh/loisi. diff --git a/LatinReader/latinreader-compose/.gitignore b/LatinReader/latinreader-compose/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/LatinReader/latinreader-compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/LatinReader/latinreader-compose/build.gradle b/LatinReader/latinreader-compose/build.gradle new file mode 100644 index 0000000..aa860f6 --- /dev/null +++ b/LatinReader/latinreader-compose/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.ericmschmidt.classicsreader.latin.compose' + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId "com.ericmschmidt.classicsreader.latin.compose" + minSdk 24 + targetSdk 36 + versionCode(21) + versionName("2.1.0") + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + kotlin { + jvmToolchain { + languageVersion = JavaLanguageVersion.of("24") + } + } +} + +dependencies { + implementation project(":core") + implementation project(":compose") + implementation project(":latinreader") + implementation 'androidx.core:core-ktx:1.17.0' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.13.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' +} \ No newline at end of file diff --git a/LatinReader/latinreader-compose/proguard-rules.pro b/LatinReader/latinreader-compose/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/LatinReader/latinreader-compose/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/LatinReader/latinreader-compose/src/androidTest/java/com/ericmschmidt/classicsreader/latin/compose/ExampleInstrumentedTest.kt b/LatinReader/latinreader-compose/src/androidTest/java/com/ericmschmidt/classicsreader/latin/compose/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..a053293 --- /dev/null +++ b/LatinReader/latinreader-compose/src/androidTest/java/com/ericmschmidt/classicsreader/latin/compose/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.ericmschmidt.classicsreader.latin.compose + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.ericmschmidt.classicsreader.latin.compose", appContext.packageName) + } +} \ No newline at end of file diff --git a/LatinReader/latinreader-compose/src/main/AndroidManifest.xml b/LatinReader/latinreader-compose/src/main/AndroidManifest.xml new file mode 100644 index 0000000..78ceb02 --- /dev/null +++ b/LatinReader/latinreader-compose/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LatinReader/latinreader/src/main/ic_launcher_foreground-playstore.png b/LatinReader/latinreader/src/main/ic_launcher_foreground-playstore.png new file mode 100644 index 0000000..c795550 Binary files /dev/null and b/LatinReader/latinreader/src/main/ic_launcher_foreground-playstore.png differ diff --git a/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/data/LatinReaderManifestDescriptions.kt b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/data/LatinReaderLibraryDescriptions.kt similarity index 100% rename from LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/data/LatinReaderManifestDescriptions.kt rename to LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/data/LatinReaderLibraryDescriptions.kt diff --git a/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Color.kt b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Color.kt new file mode 100644 index 0000000..71604ab --- /dev/null +++ b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Color.kt @@ -0,0 +1,17 @@ +package com.ericmschmidt.latin.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val Purple20 = Color(0xFF4100B3) +val Purple50 = Color(0xFFAB82FF) + +val LightGray = Color(0xFFC8C8C8) +val LightPurple = Color(0xFFE6E6FA) \ No newline at end of file diff --git a/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Theme.kt b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Theme.kt new file mode 100644 index 0000000..bb19ab5 --- /dev/null +++ b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Theme.kt @@ -0,0 +1,2 @@ +package com.ericmschmidt.latin.theme + diff --git a/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Type.kt b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Type.kt new file mode 100644 index 0000000..bb19ab5 --- /dev/null +++ b/LatinReader/latinreader/src/main/java/com/ericmschmidt/latin/theme/Type.kt @@ -0,0 +1,2 @@ +package com.ericmschmidt.latin.theme + diff --git a/LatinReader/latinreader/src/main/res/drawable/ic_latinreader.xml b/LatinReader/latinreader/src/main/res/drawable/ic_latinreader.xml new file mode 100644 index 0000000..46afa20 --- /dev/null +++ b/LatinReader/latinreader/src/main/res/drawable/ic_latinreader.xml @@ -0,0 +1,11 @@ + + + diff --git a/LatinReader/latinreader/src/main/res/drawable/ic_launcher_foreground_background.xml b/LatinReader/latinreader/src/main/res/drawable/ic_launcher_foreground_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/LatinReader/latinreader/src/main/res/drawable/ic_launcher_foreground_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LatinReader/latinreader/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground_round.xml b/LatinReader/latinreader/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground_round.xml new file mode 100644 index 0000000..3c1e05b --- /dev/null +++ b/LatinReader/latinreader/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LatinReader/latinreader/src/main/res/mipmap-hdpi/ic_launcher_fore.webp b/LatinReader/latinreader/src/main/res/mipmap-hdpi/ic_launcher_fore.webp new file mode 100644 index 0000000..cd5b834 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-hdpi/ic_launcher_fore.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-hdpi/ic_launcher_foreground_round.webp b/LatinReader/latinreader/src/main/res/mipmap-hdpi/ic_launcher_foreground_round.webp new file mode 100644 index 0000000..3e34f94 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-hdpi/ic_launcher_foreground_round.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-mdpi/ic_launcher_fore.webp b/LatinReader/latinreader/src/main/res/mipmap-mdpi/ic_launcher_fore.webp new file mode 100644 index 0000000..0c99a3e Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-mdpi/ic_launcher_fore.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-mdpi/ic_launcher_foreground_round.webp b/LatinReader/latinreader/src/main/res/mipmap-mdpi/ic_launcher_foreground_round.webp new file mode 100644 index 0000000..0241a4b Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-mdpi/ic_launcher_foreground_round.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-xhdpi/ic_launcher_fore.webp b/LatinReader/latinreader/src/main/res/mipmap-xhdpi/ic_launcher_fore.webp new file mode 100644 index 0000000..0585529 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-xhdpi/ic_launcher_fore.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-xhdpi/ic_launcher_foreground_round.webp b/LatinReader/latinreader/src/main/res/mipmap-xhdpi/ic_launcher_foreground_round.webp new file mode 100644 index 0000000..0439882 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-xhdpi/ic_launcher_foreground_round.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-xxhdpi/ic_launcher_fore.webp b/LatinReader/latinreader/src/main/res/mipmap-xxhdpi/ic_launcher_fore.webp new file mode 100644 index 0000000..58708ee Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-xxhdpi/ic_launcher_fore.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_round.webp b/LatinReader/latinreader/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_round.webp new file mode 100644 index 0000000..75ae4d8 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_round.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-xxxhdpi/ic_launcher_fore.webp b/LatinReader/latinreader/src/main/res/mipmap-xxxhdpi/ic_launcher_fore.webp new file mode 100644 index 0000000..c5492a8 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-xxxhdpi/ic_launcher_fore.webp differ diff --git a/LatinReader/latinreader/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_round.webp b/LatinReader/latinreader/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_round.webp new file mode 100644 index 0000000..d5bfca1 Binary files /dev/null and b/LatinReader/latinreader/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_round.webp differ diff --git a/LatinReader/latinreader/src/main/res/raw/caes_bg_eng.xml b/LatinReader/latinreader/src/main/res/raw/caes_bg_eng.xml index dd9775a..9541171 100755 --- a/LatinReader/latinreader/src/main/res/raw/caes_bg_eng.xml +++ b/LatinReader/latinreader/src/main/res/raw/caes_bg_eng.xml @@ -6,7 +6,8 @@ Gallic War Machine readable text C. Julius Caesar - + + C. Julius Caesar diff --git a/LatinReader/latinreader/src/main/res/raw/lucretius_lat.xml b/LatinReader/latinreader/src/main/res/raw/lucretius_lat.xml index 74b72b8..00dfee1 100755 --- a/LatinReader/latinreader/src/main/res/raw/lucretius_lat.xml +++ b/LatinReader/latinreader/src/main/res/raw/lucretius_lat.xml @@ -1150,7 +1150,6 @@

nox iter eripiet, quin ultima naturai

pervideas: ita res accendent lumina rebus.

- Liber Secundus diff --git a/LatinReader/latinreader/src/main/res/values/strings.xml b/LatinReader/latinreader/src/main/res/values/strings.xml new file mode 100644 index 0000000..d3d9e8f --- /dev/null +++ b/LatinReader/latinreader/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Latin Reader + \ No newline at end of file diff --git a/LatinReader/settings.gradle b/LatinReader/settings.gradle index 3c244c1..e665c7b 100644 --- a/LatinReader/settings.gradle +++ b/LatinReader/settings.gradle @@ -7,3 +7,5 @@ include ':views' include ':latinreader-views' include ':greekreader-views' include ':compose' +include ':latinreader-compose' +include ':greekreader-compose' diff --git a/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/LibraryFragment.kt b/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/LibraryFragment.kt index 809c738..825ec0c 100644 --- a/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/LibraryFragment.kt +++ b/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/LibraryFragment.kt @@ -32,7 +32,6 @@ import com.ericmschmidt.classicsreader.ui.interop.setContentToLazyList * @version 2.0 * @since 1.0 */ -@Suppress("DEPRECATION") class LibraryFragment : Fragment() { private var isTranslation = false diff --git a/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/ReadingFragment.kt b/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/ReadingFragment.kt index b902676..089bf13 100644 --- a/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/ReadingFragment.kt +++ b/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/fragments/ReadingFragment.kt @@ -33,7 +33,6 @@ import com.ericmschmidt.classicsreader.data.WorkInfo.WorkType * @version 2.0 * @since 1.0 */ -@Suppress("DEPRECATION") class ReadingFragment : Fragment() { private var workToGetId: String? = null diff --git a/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/interop/ComposeViewAdapter.kt b/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/interop/ComposeViewAdapter.kt index 7a2dd0b..31fbfbc 100644 --- a/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/interop/ComposeViewAdapter.kt +++ b/LatinReader/views/src/main/java/com/ericmschmidt/classicsreader/ui/interop/ComposeViewAdapter.kt @@ -14,7 +14,7 @@ import com.ericmschmidt.classicsreader.ui.fragments.LibraryFragmentDirections fun setContentToLazyList(composeView: ComposeView, library: Library, isTranslation: Boolean, activity: MainActivity) { composeView.setContent { PrettyCardLazyList(library = library, isTranslation = isTranslation, onRowClick = { selectedWork -> - navigateToReadingFragment(activity, selectedWork.id, isTranslation) + navigateToReadingFragment(activity, selectedWork.id as String, isTranslation) }) } } @@ -25,7 +25,7 @@ fun setContentToLazyList(composeView: ComposeView, library: Library, isTranslati fun setContentToLazyGrid(composeView: ComposeView, library: Library, isTranslation: Boolean, activity: MainActivity) { composeView.setContent { PrettyCardLazyVerticalGrid(library = library, isTranslation = isTranslation, onCardClick = { selectedWork -> - navigateToReadingFragment(activity, selectedWork.id, isTranslation) + navigateToReadingFragment(activity, selectedWork.id as String, isTranslation) }) } } diff --git a/LatinReader/views/src/main/res/layout/fragment_dictionary.xml b/LatinReader/views/src/main/res/layout/fragment_dictionary.xml index 5aef260..7bb7880 100755 --- a/LatinReader/views/src/main/res/layout/fragment_dictionary.xml +++ b/LatinReader/views/src/main/res/layout/fragment_dictionary.xml @@ -3,46 +3,40 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context="com.ericmschmidt.latinreader.com.ericmschmidt.classicsreader.fragments.DictionaryFragment"> + android:paddingLeft="16sp" + android:paddingRight="16sp" + tools:context="com.ericmschmidt.classicsreader.ui.fragments.DictionaryFragment"> - + android:layout_height="wrap_content" + android:inputType="text" + android:textColor="#000000" + android:hint="@string/dictionary_query_hint" + android:imeActionLabel="@string/dictionary_query_submit" + android:imeOptions="actionSearch"/> - + - - - + android:layout_height="wrap_content" + android:layout_centerInParent="true"/> - + - - - - + + + diff --git a/LatinReader/views/src/main/res/layout/fragment_help.xml b/LatinReader/views/src/main/res/layout/fragment_help.xml index 894d83b..1b407d0 100644 --- a/LatinReader/views/src/main/res/layout/fragment_help.xml +++ b/LatinReader/views/src/main/res/layout/fragment_help.xml @@ -6,7 +6,7 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" - tools:context="com.ericmschmidt.latinreader.com.ericmschmidt.classicsreader.fragments.HelpFragment"> + tools:context="com.ericmschmidt.classicsreader.ui.fragments.HelpFragment"> + android:paddingTop="@dimen/fab_margin" + android:paddingLeft="@dimen/fab_margin" + android:paddingRight="@dimen/fab_margin" + tools:context="com.ericmschmidt.classicsreader.ui.fragments.VocabularyFragment"> + - + android:layout_height="match_parent"> - + android:layout_centerInParent="true"/> - + - - - - - - - - - - + android:layout_height="wrap_content" /> + + diff --git a/LatinReader/views/src/main/res/layout/nav_header_main.xml b/LatinReader/views/src/main/res/layout/nav_header_main.xml index 9361eda..ea56c83 100755 --- a/LatinReader/views/src/main/res/layout/nav_header_main.xml +++ b/LatinReader/views/src/main/res/layout/nav_header_main.xml @@ -17,6 +17,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="@dimen/nav_header_vertical_spacing" + android:contentDescription="@string/app_name" android:src="@mipmap/ic_launcher"/>