74 |
75 | suspend fun getByParam(
76 | param: String, value: P
77 | ) = getByParam(SimpleSQLiteQuery("$baseWhereQuery $param='$value'"))
78 |
79 | suspend fun getByParams(
80 | paramPair: Pair,
81 | vararg paramPairs: Pair
82 | ): List {
83 | val params = listOf(paramPair, *paramPairs)
84 | val condition = buildString {
85 | params.forEachIndexed { index, pair ->
86 | append("${pair.first}='${pair.second}'")
87 | if (index != params.lastIndex) append(" and ")
88 | }
89 | }
90 | return getByParam(SimpleSQLiteQuery("$baseWhereQuery $condition"))
91 | }
92 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/MainTopBar.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * MainTopBar.kt Created by Yamin Siahmargooei at 2022/9/19
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content
22 |
23 | import androidx.compose.material.icons.Icons
24 | import androidx.compose.material.icons.twotone.*
25 | import androidx.compose.material3.*
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.unit.dp
29 | import io.github.yamin8000.owl.R
30 | import io.github.yamin8000.owl.ui.composable.AnimatedAppIcon
31 | import io.github.yamin8000.owl.ui.composable.ClickableIcon
32 |
33 | @OptIn(ExperimentalMaterial3Api::class)
34 | @Composable
35 | fun MainTopBar(
36 | scrollBehavior: TopAppBarScrollBehavior,
37 | onHistoryClick: () -> Unit,
38 | onFavouritesClick: () -> Unit,
39 | onRandomWordClick: () -> Unit,
40 | onSettingsClick: () -> Unit,
41 | onInfoClick: () -> Unit,
42 | ) {
43 | Surface(
44 | shadowElevation = 8.dp,
45 | content = {
46 | TopAppBar(
47 | scrollBehavior = scrollBehavior,
48 | title = { AnimatedAppIcon() },
49 | actions = {
50 | ClickableIcon(
51 | imageVector = Icons.TwoTone.History,
52 | contentDescription = stringResource(R.string.search_history),
53 | onClick = onHistoryClick,
54 | )
55 |
56 | ClickableIcon(
57 | imageVector = Icons.TwoTone.Favorite,
58 | contentDescription = stringResource(R.string.favourites),
59 | onClick = onFavouritesClick,
60 | )
61 |
62 | ClickableIcon(
63 | imageVector = Icons.TwoTone.Casino,
64 | contentDescription = stringResource(R.string.random_word),
65 | onClick = onRandomWordClick,
66 | )
67 |
68 | ClickableIcon(
69 | imageVector = Icons.TwoTone.Settings,
70 | contentDescription = stringResource(R.string.settings),
71 | onClick = onSettingsClick
72 | )
73 |
74 | ClickableIcon(
75 | imageVector = Icons.TwoTone.Info,
76 | contentDescription = stringResource(R.string.about_app),
77 | onClick = onInfoClick
78 | )
79 | }
80 | )
81 | }
82 | )
83 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/history/HistoryState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * HistoryState.kt Created by Yamin Siahmargooei at 2022/8/24
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content.history
22 |
23 | import android.content.Context
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.MutableState
26 | import androidx.compose.runtime.mutableStateOf
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.runtime.saveable.rememberSaveable
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.platform.LocalLifecycleOwner
31 | import androidx.datastore.preferences.core.edit
32 | import androidx.datastore.preferences.core.stringPreferencesKey
33 | import androidx.lifecycle.LifecycleCoroutineScope
34 | import androidx.lifecycle.lifecycleScope
35 | import io.github.yamin8000.owl.content.historyDataStore
36 | import io.github.yamin8000.owl.util.list.ListSatiation
37 | import kotlinx.coroutines.launch
38 |
39 | class HistoryState(
40 | private val context: Context,
41 | val lifeCycleScope: LifecycleCoroutineScope,
42 | var history: MutableState>
43 | ) {
44 |
45 | init {
46 | lifeCycleScope.launch { fetchHistory() }
47 | }
48 |
49 | val listSatiation: ListSatiation
50 | get() = when (history.value.size) {
51 | 0 -> ListSatiation.Empty
52 | else -> ListSatiation.Partial
53 | }
54 |
55 | private suspend fun fetchHistory() {
56 | context.historyDataStore.data.collect { preferences ->
57 | val newSet = history.value.toMutableSet()
58 | preferences.asMap().forEach { entry -> newSet.add(entry.value.toString()) }
59 | history.value = newSet
60 | }
61 | }
62 |
63 | suspend fun removeSingleHistory(
64 | singleHistory: String
65 | ) {
66 | context.historyDataStore.edit { it.remove(stringPreferencesKey(singleHistory)) }
67 | val newSet = history.value.toMutableSet()
68 | newSet.remove(singleHistory)
69 | history.value = newSet
70 | }
71 |
72 | suspend fun removeAllHistory() {
73 | context.historyDataStore.edit { it.clear() }
74 | history.value = emptySet()
75 | }
76 | }
77 |
78 | @Composable
79 | fun rememberHistoryState(
80 | context: Context = LocalContext.current,
81 | lifeCycleScope: LifecycleCoroutineScope = LocalLifecycleOwner.current.lifecycleScope,
82 | history: MutableState> = rememberSaveable { mutableStateOf(emptySet()) }
83 | ) = remember(context, lifeCycleScope, history) { HistoryState(context, lifeCycleScope, history) }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fa/strings.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | جغدک
23 | جستجو
24 | واژهای برای جستجو بنویسید
25 | واژههای برگزیده
26 | جستجوهای پیشین
27 | واژه تصادفی
28 | خطا در برقراری ارتباط با اینترنت، خطا ممکن است به خاطر محدودیت های اینترنت باشد
29 | سرور برنامه تحت فشار است، لطفا چند دقیقه دیگر تلاش کنید
30 | توضیحی برای این کلمه یافت نشد
31 | خطای احراز هویت با سرور برنامه
32 | متن کپی شد
33 | این یک برنامه آزاد طبق پروانه گنو است که توضیحات بیشتر آن در ادامه آورده شده است
34 | لوگو پروانه برنامه GPLv3
35 | فرهنگ واژگان انگلیسی به انگلیسی جغد، برگرفته از فرهنگ واژگان
36 | به برگزیدهها اضافه شد
37 | پاک کردن
38 | خطای پیش بینی نشده
39 | عبارتی وارد نشده
40 | اشتراک گذاری
41 | این متن توسط برنامه جغدک تولید شده
42 | پاک کردن همه
43 | تنظیمات
44 | زمینه
45 | در نسخه های اندروید 12 به بعد زمینه برنامه بر اساس رنگ قالب تصویر پس زمینه گوشی شما تعیین می شود
46 | هماهنگ سیستم
47 | روشن
48 | تاریک
49 | زبان متن به گفتار
50 | درباره
51 | نگارش
52 | تلفظ
53 | واژه
54 | اموجی
55 | نمونه
56 | معنی
57 | نوع
58 | عمومی
59 | لرزش در زمان پیمایش لیست
60 | لغو
61 | این واژه پیش از این به برگزیدهها اضافه شده است.
62 | پاک کردن
63 | صدای بلندگو را زیاد کنید.
64 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/settings/SettingsState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * SettingsState.kt Created by Yamin Siahmargooei at 2022/9/20
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content.settings
22 |
23 | import android.content.Context
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.MutableState
26 | import androidx.compose.runtime.mutableStateOf
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.runtime.saveable.rememberSaveable
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.platform.LocalLifecycleOwner
31 | import androidx.lifecycle.LifecycleCoroutineScope
32 | import androidx.lifecycle.lifecycleScope
33 | import io.github.yamin8000.owl.content.settingsDataStore
34 | import io.github.yamin8000.owl.util.Constants
35 | import io.github.yamin8000.owl.util.DataStoreHelper
36 | import kotlinx.coroutines.launch
37 | import java.util.Locale
38 |
39 | class SettingsState(
40 | context: Context,
41 | val scope: LifecycleCoroutineScope,
42 | val themeSetting: MutableState,
43 | var ttsLang: MutableState,
44 | val isVibrating: MutableState
45 | ) {
46 | private val dataStore = DataStoreHelper(context.settingsDataStore)
47 |
48 | init {
49 | scope.launch {
50 | themeSetting.value = ThemeSetting.valueOf(
51 | dataStore.getString(Constants.THEME) ?: ThemeSetting.System.name
52 | )
53 | ttsLang.value = dataStore.getString(Constants.TTS_LANG) ?: Locale.US.toLanguageTag()
54 | isVibrating.value = dataStore.getBool(Constants.IS_VIBRATING) ?: true
55 | }
56 | }
57 |
58 | suspend fun updateTtsLang(
59 | newTtsLang: String
60 | ) {
61 | ttsLang.value = newTtsLang
62 | dataStore.setString(Constants.TTS_LANG, newTtsLang)
63 | }
64 |
65 | suspend fun updateThemeSetting(
66 | newTheme: ThemeSetting
67 | ) {
68 | themeSetting.value = newTheme
69 | dataStore.setString(Constants.THEME, newTheme.name)
70 | }
71 |
72 | suspend fun updateVibrationSetting(
73 | newVibrationSetting: Boolean
74 | ) {
75 | isVibrating.value = newVibrationSetting
76 | dataStore.setBool(Constants.IS_VIBRATING, newVibrationSetting)
77 | }
78 | }
79 |
80 | @Composable
81 | fun rememberSettingsState(
82 | context: Context = LocalContext.current,
83 | coroutineScope: LifecycleCoroutineScope = LocalLifecycleOwner.current.lifecycleScope,
84 | themeSetting: MutableState = rememberSaveable { mutableStateOf(ThemeSetting.System) },
85 | ttsLang: MutableState = rememberSaveable { mutableStateOf(Locale.US.toLanguageTag()) },
86 | isVibrating: MutableState = rememberSaveable { mutableStateOf(true) }
87 | ) = remember(context, themeSetting, coroutineScope, ttsLang, isVibrating) {
88 | SettingsState(context, coroutineScope, themeSetting, ttsLang, isVibrating)
89 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-hu/strings.xml:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 | Owl
24 | Keresés
25 | Írj be egy szót a kereséshez
26 | Kedvenc Szavak
27 | Keresési Előzmények
28 | Véletlenszerű Szó
29 | Nincs kapcsolat a szerverrel. Az adatok előfordulhat, hogy a gyorsítótárból kerülnek betöltésre.
30 | A szerver elfoglalt, próbáld újra!
31 | Nem található definíció a szóra.
32 | Hitelesítési Hiba
33 | Szöveg másolva.
34 | Ez egy ingyenes applikáció a GNU licensz alatt, tudj meg többet:
35 | GPLv3 Licensz Logó
36 | Angol-Angol szótár alapján:
37 | Hozzáadva a kedvencekhez
38 | Kiürítés
39 | Váratlan hiba!
40 | Semmi sem került bevitelre.
41 | Megosztás
42 | Ezt a szöveget az Owl app generálta
43 | Mind kiürítése
44 | Beállítások
45 | Téma
46 | Android 12-n és afelett az alkalmazásod témája a háttérképed színei alapján készül el
47 | Rendszer
48 | Világos
49 | Sötét
50 | Szövegből-Beszéd Nyelve
51 | Az alkalmazásról
52 | Verzió
53 | Kiejtés
54 | Szó
55 | Emoji
56 | Példa
57 | Definíció
58 | Típus
59 | Általános
60 | Rezgés görgetéskor
61 | Mégse
62 | Ez a szó már a kedvencek között van.
63 | Törlés
64 | Növeld meg a hangerődet
65 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | Owl
23 | Search
24 | Enter a word to search
25 | Favourite Words
26 | Search History
27 | Random Word
28 | No connection to the server. Data maybe loaded from the cache.
29 | Server is busy, try again!
30 | No definition for the word found.
31 | Authentication Error
32 | Text copied.
33 | This is a free application under GNU license, see more:
34 | GPLv3 License Logo
35 | English to English Dictionary, based on:
36 | https://github.com/yamin8000/Owl2
37 | https://raw.githubusercontent.com/yamin8000/Owl2/master/LICENSE
38 | https://owlbot.info
39 | Added to favourites
40 | Clear
41 | Unexpected error!
42 | Nothing is entered.
43 | Share
44 | This text is generated using Owl app
45 | Clear All
46 | Settings
47 | Theme
48 | On Android 12+ your app theme colors are based on your home screen background
49 | System
50 | Light
51 | Dark
52 | Text-to-Speech Language
53 | About
54 | Version
55 | Pronunciation
56 | Word
57 | Emoji
58 | Example
59 | Definition
60 | Type
61 | This text is extracted from Owlbot Dictionary.
62 | General
63 | Vibrate on scroll
64 | Cancel
65 | This word is already added to the favorites.
66 | Delete
67 | Increase your volume
68 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/favourites/Favourites.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Favourites.kt Created by Yamin Siahmargooei at 2022/8/22
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content.favourites
22 |
23 | import androidx.compose.foundation.layout.Arrangement
24 | import androidx.compose.foundation.layout.fillMaxWidth
25 | import androidx.compose.foundation.lazy.grid.GridCells
26 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
27 | import androidx.compose.foundation.lazy.grid.items
28 | import androidx.compose.material3.ExperimentalMaterial3Api
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.runtime.saveable.rememberSaveable
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.res.stringResource
33 | import androidx.compose.ui.unit.dp
34 | import io.github.yamin8000.owl.R
35 | import io.github.yamin8000.owl.ui.composable.EmptyList
36 | import io.github.yamin8000.owl.ui.composable.RemovableCard
37 | import io.github.yamin8000.owl.ui.composable.ScaffoldWithTitle
38 | import io.github.yamin8000.owl.util.list.ListSatiation
39 | import kotlinx.coroutines.launch
40 |
41 | @OptIn(ExperimentalMaterial3Api::class)
42 | @Composable
43 | fun FavouritesContent(
44 | onFavouritesItemClick: (String) -> Unit,
45 | onBackClick: () -> Unit
46 | ) {
47 | val state = rememberFavouritesState()
48 |
49 | ScaffoldWithTitle(
50 | title = stringResource(R.string.favourites),
51 | onBackClick = onBackClick
52 | ) {
53 | when (state.listSatiation) {
54 | ListSatiation.Empty -> EmptyList()
55 | ListSatiation.Partial -> {
56 | FavouritesGrid(
57 | favourites = state.favourites.value.toList(),
58 | onItemClick = onFavouritesItemClick,
59 | onItemLongClick = { favourite ->
60 | state.scope.launch { state.removeFavourite(favourite) }
61 | }
62 | )
63 | }
64 | }
65 | }
66 | }
67 |
68 | @Composable
69 | fun FavouritesGrid(
70 | favourites: List,
71 | onItemClick: (String) -> Unit,
72 | onItemLongClick: (String) -> Unit
73 | ) {
74 | val span = rememberSaveable { if (favourites.size == 1) 1 else 2 }
75 | LazyVerticalGrid(
76 | modifier = Modifier.fillMaxWidth(),
77 | horizontalArrangement = Arrangement.spacedBy(8.dp),
78 | verticalArrangement = Arrangement.spacedBy(8.dp),
79 | columns = GridCells.Fixed(span),
80 | content = {
81 | items(
82 | items = favourites,
83 | itemContent = { favourite ->
84 | FavouriteItem(
85 | favourite = favourite,
86 | onClick = onItemClick,
87 | onLongClick = { onItemLongClick(favourite) }
88 | )
89 | }
90 | )
91 | }
92 | )
93 | }
94 |
95 | @Composable
96 | private fun FavouriteItem(
97 | favourite: String,
98 | onClick: (String) -> Unit,
99 | onLongClick: () -> Unit
100 | ) {
101 | RemovableCard(
102 | item = favourite,
103 | onClick = { onClick(favourite) },
104 | onLongClick = onLongClick
105 | )
106 | }
--------------------------------------------------------------------------------
/app/schemas/io.github.yamin8000.owl.db.AppDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "0c3ad9bffebaa41fcb44752f733c24f4",
6 | "entities": [
7 | {
8 | "tableName": "WordEntity",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `word` TEXT NOT NULL, `pronunciation` TEXT)",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "word",
19 | "columnName": "word",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "pronunciation",
25 | "columnName": "pronunciation",
26 | "affinity": "TEXT",
27 | "notNull": false
28 | }
29 | ],
30 | "primaryKey": {
31 | "columnNames": [
32 | "id"
33 | ],
34 | "autoGenerate": true
35 | },
36 | "indices": [],
37 | "foreignKeys": []
38 | },
39 | {
40 | "tableName": "DefinitionEntity",
41 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `wordId` INTEGER NOT NULL, `type` TEXT, `definition` TEXT NOT NULL, `example` TEXT, `imageUrl` TEXT, `emoji` TEXT, FOREIGN KEY(`wordId`) REFERENCES `WordEntity`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
42 | "fields": [
43 | {
44 | "fieldPath": "id",
45 | "columnName": "id",
46 | "affinity": "INTEGER",
47 | "notNull": true
48 | },
49 | {
50 | "fieldPath": "wordId",
51 | "columnName": "wordId",
52 | "affinity": "INTEGER",
53 | "notNull": true
54 | },
55 | {
56 | "fieldPath": "type",
57 | "columnName": "type",
58 | "affinity": "TEXT",
59 | "notNull": false
60 | },
61 | {
62 | "fieldPath": "definition",
63 | "columnName": "definition",
64 | "affinity": "TEXT",
65 | "notNull": true
66 | },
67 | {
68 | "fieldPath": "example",
69 | "columnName": "example",
70 | "affinity": "TEXT",
71 | "notNull": false
72 | },
73 | {
74 | "fieldPath": "imageUrl",
75 | "columnName": "imageUrl",
76 | "affinity": "TEXT",
77 | "notNull": false
78 | },
79 | {
80 | "fieldPath": "emoji",
81 | "columnName": "emoji",
82 | "affinity": "TEXT",
83 | "notNull": false
84 | }
85 | ],
86 | "primaryKey": {
87 | "columnNames": [
88 | "id"
89 | ],
90 | "autoGenerate": true
91 | },
92 | "indices": [
93 | {
94 | "name": "index_DefinitionEntity_wordId",
95 | "unique": false,
96 | "columnNames": [
97 | "wordId"
98 | ],
99 | "orders": [],
100 | "createSql": "CREATE INDEX IF NOT EXISTS `index_DefinitionEntity_wordId` ON `${TABLE_NAME}` (`wordId`)"
101 | }
102 | ],
103 | "foreignKeys": [
104 | {
105 | "table": "WordEntity",
106 | "onDelete": "RESTRICT",
107 | "onUpdate": "NO ACTION",
108 | "columns": [
109 | "wordId"
110 | ],
111 | "referencedColumns": [
112 | "id"
113 | ]
114 | }
115 | ]
116 | }
117 | ],
118 | "views": [],
119 | "setupQueries": [
120 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
121 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c3ad9bffebaa41fcb44752f733c24f4')"
122 | ]
123 | }
124 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Color.kt Created by Yamin Siahmargooei at 2022/6/16
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.ui.theme
22 |
23 | import androidx.compose.ui.graphics.Color
24 |
25 | val md_theme_light_primary = Color(0xFFBB0055)
26 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
27 | val md_theme_light_primaryContainer = Color(0xFFFFC4CE)
28 | val md_theme_light_onPrimaryContainer = Color(0xFF3F0018)
29 | val md_theme_light_secondary = Color(0xFF75565C)
30 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
31 | val md_theme_light_secondaryContainer = Color(0xFFFFD9DF)
32 | val md_theme_light_onSecondaryContainer = Color(0xFF2B151A)
33 | val md_theme_light_tertiary = Color(0xFF7A5733)
34 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
35 | val md_theme_light_tertiaryContainer = Color(0xFFFFDCBD)
36 | val md_theme_light_onTertiaryContainer = Color(0xFF2C1600)
37 | val md_theme_light_error = Color(0xFFBA1A1A)
38 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
39 | val md_theme_light_onError = Color(0xFFFFFFFF)
40 | val md_theme_light_onErrorContainer = Color(0xFF410002)
41 | val md_theme_light_background = Color(0xFFFBE9E7)
42 | val md_theme_light_onBackground = Color(0xFF201A1B)
43 | val md_theme_light_surface = Color(0xFFFBE9E7)
44 | val md_theme_light_onSurface = Color(0xFF201A1B)
45 | val md_theme_light_surfaceVariant = Color(0xFFF3DDE0)
46 | val md_theme_light_onSurfaceVariant = Color(0xFF524346)
47 | val md_theme_light_outline = Color(0xFF847375)
48 | val md_theme_light_inverseOnSurface = Color(0xFFFAEEEF)
49 | val md_theme_light_inverseSurface = Color(0xFF352F30)
50 | val md_theme_light_inversePrimary = Color(0xFFFFB1C2)
51 | //val md_theme_light_shadow = Color(0xFF000000)
52 | val md_theme_light_surfaceTint = Color(0xFFBB0055)
53 | //val md_theme_light_surfaceTintColor = Color(0xFFBB0055)
54 |
55 | val md_theme_dark_primary = Color(0xFFFFB1C2)
56 | val md_theme_dark_onPrimary = Color(0xFF66002B)
57 | val md_theme_dark_primaryContainer = Color(0xFF8F0040)
58 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9DF)
59 | val md_theme_dark_secondary = Color(0xFFE4BDC4)
60 | val md_theme_dark_onSecondary = Color(0xFF43292F)
61 | val md_theme_dark_secondaryContainer = Color(0xFF5B3F45)
62 | val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9DF)
63 | val md_theme_dark_tertiary = Color(0xFFECBE91)
64 | val md_theme_dark_onTertiary = Color(0xFF462A09)
65 | val md_theme_dark_tertiaryContainer = Color(0xFF60401E)
66 | val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCBD)
67 | val md_theme_dark_error = Color(0xFFFFB4AB)
68 | val md_theme_dark_errorContainer = Color(0xFF93000A)
69 | val md_theme_dark_onError = Color(0xFF690005)
70 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
71 | val md_theme_dark_background = Color(0xFF201A1B)
72 | val md_theme_dark_onBackground = Color(0xFFECE0E0)
73 | val md_theme_dark_surface = Color(0xFF201A1B)
74 | val md_theme_dark_onSurface = Color(0xFFECE0E0)
75 | val md_theme_dark_surfaceVariant = Color(0xFF524346)
76 | val md_theme_dark_onSurfaceVariant = Color(0xFFD6C2C4)
77 | val md_theme_dark_outline = Color(0xFF9E8C8F)
78 | val md_theme_dark_inverseOnSurface = Color(0xFF201A1B)
79 | val md_theme_dark_inverseSurface = Color(0xFFECE0E0)
80 | val md_theme_dark_inversePrimary = Color(0xFFBB0055)
81 | //val md_theme_dark_shadow = Color(0xFF000000)
82 | val md_theme_dark_surfaceTint = Color(0xFFFFB1C2)
83 | //val md_theme_dark_surfaceTintColor = Color(0xFFFFB1C2)
84 |
85 |
86 | //val seed = Color(0xFFBB0055)
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/ui/composable/Cards.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Cards.kt Created by Yamin Siahmargooei at 2022/8/24
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.ui.composable
22 |
23 | import androidx.compose.foundation.layout.Arrangement
24 | import androidx.compose.foundation.layout.Column
25 | import androidx.compose.foundation.layout.ColumnScope
26 | import androidx.compose.foundation.layout.fillMaxWidth
27 | import androidx.compose.foundation.layout.padding
28 | import androidx.compose.material3.Card
29 | import androidx.compose.material3.MaterialTheme
30 | import androidx.compose.material3.Text
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.runtime.mutableStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.runtime.setValue
36 | import androidx.compose.ui.Alignment
37 | import androidx.compose.ui.Modifier
38 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
39 | import androidx.compose.ui.platform.LocalHapticFeedback
40 | import androidx.compose.ui.text.style.TextAlign
41 | import androidx.compose.ui.unit.dp
42 | import androidx.compose.ui.unit.sp
43 | import io.github.yamin8000.owl.ui.theme.DefaultCutShape
44 |
45 | @Composable
46 | fun SettingsItemCard(
47 | modifier: Modifier = Modifier,
48 | columnModifier: Modifier = Modifier,
49 | title: String,
50 | content: @Composable ColumnScope.() -> Unit
51 | ) {
52 | Column(
53 | verticalArrangement = Arrangement.spacedBy(4.dp),
54 | horizontalAlignment = Alignment.Start,
55 | content = {
56 | PersianText(
57 | text = title,
58 | fontSize = 18.sp,
59 | color = MaterialTheme.colorScheme.primary
60 | )
61 | Card(
62 | modifier = modifier,
63 | shape = DefaultCutShape,
64 | content = {
65 | Column(
66 | modifier = columnModifier.padding(16.dp),
67 | verticalArrangement = Arrangement.spacedBy(8.dp),
68 | horizontalAlignment = Alignment.CenterHorizontally,
69 | content = { content() }
70 | )
71 | }
72 | )
73 | },
74 | )
75 | }
76 |
77 | @Composable
78 | fun RemovableCard(
79 | item: String,
80 | onClick: () -> Unit,
81 | onLongClick: () -> Unit
82 | ) {
83 | var isMenuExpanded by remember { mutableStateOf(false) }
84 | val haptic = LocalHapticFeedback.current
85 | Card(
86 | shape = DefaultCutShape,
87 | content = {
88 | Ripple(
89 | modifier = Modifier.padding(8.dp),
90 | onClick = onClick,
91 | onLongClick = { isMenuExpanded = true },
92 | content = {
93 | Text(
94 | text = item,
95 | textAlign = TextAlign.Center,
96 | modifier = Modifier
97 | .padding(16.dp)
98 | .fillMaxWidth()
99 | )
100 | }
101 | )
102 | DeleteMenu(
103 | expanded = isMenuExpanded,
104 | onDismiss = { isMenuExpanded = false },
105 | onDelete = {
106 | isMenuExpanded = false
107 | haptic.performHapticFeedback(HapticFeedbackType.LongPress)
108 | onLongClick()
109 | }
110 | )
111 | }
112 | )
113 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/util/AutoCompleteHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * AutoCompleteHelper.kt Created by Yamin Siahmargooei at 2022/10/22
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.util
22 |
23 | import android.content.Context
24 | import android.content.res.Resources.NotFoundException
25 | import io.github.yamin8000.owl.R
26 | import io.github.yamin8000.owl.util.Constants.DEFAULT_N_GRAM_SIZE
27 | import io.github.yamin8000.owl.util.Constants.NOT_WORD_CHARS_REGEX
28 | import io.github.yamin8000.owl.util.Constants.db
29 | import kotlinx.coroutines.CoroutineScope
30 | import kotlinx.coroutines.launch
31 | import kotlin.math.abs
32 | import kotlin.math.ceil
33 | import kotlin.math.roundToInt
34 |
35 | class AutoCompleteHelper(
36 | private val context: Context,
37 | coroutineScope: CoroutineScope,
38 | userData: List = listOf()
39 | ) {
40 | private var data = setOf()
41 |
42 | init {
43 | data = getBasic2000Data().plus(userData).toSet()
44 | coroutineScope.launch { data = data.plus(getOldSearchData()) }
45 | }
46 |
47 | private fun getBasic2000Data() = try {
48 | context.resources.openRawResource(R.raw.basic2000)
49 | .bufferedReader()
50 | .use { it.readText() }
51 | .split(',')
52 | .map { it.replace(NOT_WORD_CHARS_REGEX, "") }
53 | } catch (e: NotFoundException) {
54 | listOf()
55 | }
56 |
57 | suspend fun suggestTermsForSearch(
58 | searchTerm: String
59 | ): List {
60 | data = data.plus(getOldSearchData())
61 |
62 | val term = searchTerm.lowercase().replace(NOT_WORD_CHARS_REGEX, "")
63 | val nGramSize = nGramSizeProvider(term)
64 | if (data.contains(term)) return listOf(term)
65 | val searchTermGrams = term.windowed(nGramSize)
66 | val suggestions = buildSet {
67 | searchTermGrams.forEach { gram ->
68 | addAll(data.filter { word -> word.contains(gram) })
69 | }
70 | }
71 | return sortSuggestions(suggestions, term)
72 | }
73 |
74 | private fun sortSuggestions(
75 | suggestions: Set,
76 | searchTerm: String
77 | ): List {
78 | val nGramSize = nGramSizeProvider(searchTerm)
79 | if (suggestions.isNotEmpty() && suggestions.size > 1) {
80 | val searchTermGrams = searchTerm.windowed(nGramSize)
81 | val rankedSuggestions = buildList {
82 | suggestions.forEach { suggestion ->
83 | val rank = suggestion.windowed(nGramSize)
84 | .intersect(searchTermGrams.toSet())
85 | .size
86 | add(rank to suggestion)
87 | }
88 | }
89 | return rankedSuggestions.asSequence()
90 | .sortedBy { abs(it.second.length - searchTerm.length) }
91 | .sortedByDescending {
92 | it.second.startsWith(searchTerm.take(nGramSize)) ||
93 | it.second.endsWith(searchTerm.takeLast(nGramSize))
94 | }
95 | .sortedByDescending { it.first }
96 | .map { it.second }
97 | .toList()
98 | } else return suggestions.toList()
99 | }
100 |
101 | private fun nGramSizeProvider(
102 | searchTerm: String
103 | ): Int {
104 | return if (searchTerm.length > DEFAULT_N_GRAM_SIZE) {
105 | val size = ceil(searchTerm.length.toFloat() / DEFAULT_N_GRAM_SIZE).roundToInt()
106 | if (size < DEFAULT_N_GRAM_SIZE) DEFAULT_N_GRAM_SIZE
107 | else size
108 | } else DEFAULT_N_GRAM_SIZE
109 | }
110 |
111 | private suspend fun getOldSearchData() = db.wordDao().getAll().map { it.word }
112 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/history/History.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * History.kt Created by Yamin Siahmargooei at 2022/8/24
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content.history
22 |
23 | import androidx.compose.foundation.layout.Arrangement
24 | import androidx.compose.foundation.lazy.grid.GridCells
25 | import androidx.compose.foundation.lazy.grid.GridItemSpan
26 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
27 | import androidx.compose.material3.Button
28 | import androidx.compose.material3.ExperimentalMaterial3Api
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.ui.res.stringResource
31 | import androidx.compose.ui.unit.dp
32 | import io.github.yamin8000.owl.R
33 | import io.github.yamin8000.owl.ui.composable.*
34 | import io.github.yamin8000.owl.ui.theme.DefaultCutShape
35 | import io.github.yamin8000.owl.util.list.ListSatiation
36 | import kotlinx.coroutines.launch
37 |
38 | @OptIn(ExperimentalMaterial3Api::class)
39 | @Composable
40 | fun HistoryContent(
41 | onHistoryItemClick: (String) -> Unit,
42 | onBackClick: () -> Unit
43 | ) {
44 | val state = rememberHistoryState()
45 |
46 | ScaffoldWithTitle(
47 | title = stringResource(R.string.search_history),
48 | onBackClick = onBackClick,
49 | content = {
50 | when (state.listSatiation) {
51 | ListSatiation.Empty -> EmptyList()
52 | ListSatiation.Partial -> {
53 | val list = state.history.value.toList()
54 | LazyVerticalGrid(
55 | horizontalArrangement = Arrangement.spacedBy(8.dp),
56 | verticalArrangement = Arrangement.spacedBy(8.dp),
57 | columns = GridCells.Fixed(2),
58 | content = {
59 | item(
60 | span = { GridItemSpan(maxCurrentLineSpan) },
61 | content = {
62 | RemoveAlHistoryButton {
63 | state.lifeCycleScope.launch { state.removeAllHistory() }
64 | }
65 | }
66 | )
67 | items(
68 | span = { GridItemSpan(1) },
69 | count = list.size,
70 | itemContent = {
71 | HistoryItem(
72 | history = list[it],
73 | onClick = onHistoryItemClick,
74 | onLongClick = {
75 | state.lifeCycleScope.launch {
76 | state.removeSingleHistory(it)
77 | }
78 | }
79 | )
80 | }
81 | )
82 | }
83 | )
84 | }
85 | }
86 | }
87 | )
88 | }
89 |
90 | @Composable
91 | private fun RemoveAlHistoryButton(
92 | onRemoveAllClick: () -> Unit
93 | ) {
94 | Button(
95 | onClick = onRemoveAllClick,
96 | shape = DefaultCutShape,
97 | content = { PersianText(text = stringResource(R.string.clear_all)) }
98 | )
99 | }
100 |
101 | @Composable
102 | fun HistoryItem(
103 | history: String,
104 | onClick: (String) -> Unit,
105 | onLongClick: (String) -> Unit
106 | ) {
107 | RemovableCard(
108 | item = history,
109 | onClick = { onClick.invoke(history) },
110 | onLongClick = { onLongClick(history) }
111 | )
112 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Type.kt Created by Yamin Siahmargooei at 2022/6/16
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.ui.theme
22 |
23 | import androidx.compose.material3.Typography
24 | import androidx.compose.ui.text.TextStyle
25 | import androidx.compose.ui.text.font.Font
26 | import androidx.compose.ui.text.font.FontFamily
27 | import androidx.compose.ui.text.font.FontWeight
28 | import androidx.compose.ui.unit.sp
29 | import io.github.yamin8000.owl.R
30 |
31 | val Samim = FontFamily(Font(R.font.samimbold))
32 |
33 | val Roboto = FontFamily.Default
34 |
35 | val AppTypography = Typography(
36 | labelLarge = TextStyle(
37 | fontFamily = Roboto,
38 | fontWeight = FontWeight.Medium,
39 | letterSpacing = 0.10000000149011612.sp,
40 | lineHeight = 20.sp,
41 | fontSize = 14.sp
42 | ),
43 | labelMedium = TextStyle(
44 | fontFamily = Roboto,
45 | fontWeight = FontWeight.Medium,
46 | letterSpacing = 0.5.sp,
47 | lineHeight = 16.sp,
48 | fontSize = 12.sp
49 | ),
50 | labelSmall = TextStyle(
51 | fontFamily = Roboto,
52 | fontWeight = FontWeight.Medium,
53 | letterSpacing = 0.5.sp,
54 | lineHeight = 16.sp,
55 | fontSize = 11.sp
56 | ),
57 | bodyLarge = TextStyle(
58 | fontFamily = Roboto,
59 | fontWeight = FontWeight.W400,
60 | letterSpacing = 0.5.sp,
61 | lineHeight = 24.sp,
62 | fontSize = 16.sp
63 | ),
64 | bodyMedium = TextStyle(
65 | fontFamily = Roboto,
66 | fontWeight = FontWeight.W400,
67 | letterSpacing = 0.25.sp,
68 | lineHeight = 20.sp,
69 | fontSize = 14.sp
70 | ),
71 | bodySmall = TextStyle(
72 | fontFamily = Roboto,
73 | fontWeight = FontWeight.W400,
74 | letterSpacing = 0.4000000059604645.sp,
75 | lineHeight = 16.sp,
76 | fontSize = 12.sp
77 | ),
78 | headlineLarge = TextStyle(
79 | fontFamily = Roboto,
80 | fontWeight = FontWeight.W400,
81 | letterSpacing = 0.sp,
82 | lineHeight = 40.sp,
83 | fontSize = 32.sp
84 | ),
85 | headlineMedium = TextStyle(
86 | fontFamily = Roboto,
87 | fontWeight = FontWeight.W400,
88 | letterSpacing = 0.sp,
89 | lineHeight = 36.sp,
90 | fontSize = 28.sp
91 | ),
92 | headlineSmall = TextStyle(
93 | fontFamily = Roboto,
94 | fontWeight = FontWeight.W400,
95 | letterSpacing = 0.sp,
96 | lineHeight = 32.sp,
97 | fontSize = 24.sp
98 | ),
99 | displayLarge = TextStyle(
100 | fontFamily = Roboto,
101 | fontWeight = FontWeight.W400,
102 | letterSpacing = (-0.25).sp,
103 | lineHeight = 64.sp,
104 | fontSize = 57.sp
105 | ),
106 | displayMedium = TextStyle(
107 | fontFamily = Roboto,
108 | fontWeight = FontWeight.W400,
109 | letterSpacing = 0.sp,
110 | lineHeight = 52.sp,
111 | fontSize = 45.sp
112 | ),
113 | displaySmall = TextStyle(
114 | fontFamily = Roboto,
115 | fontWeight = FontWeight.W400,
116 | letterSpacing = 0.sp,
117 | lineHeight = 44.sp,
118 | fontSize = 36.sp
119 | ),
120 | titleLarge = TextStyle(
121 | fontFamily = Roboto,
122 | fontWeight = FontWeight.W400,
123 | letterSpacing = 0.sp,
124 | lineHeight = 28.sp,
125 | fontSize = 22.sp
126 | ),
127 | titleMedium = TextStyle(
128 | fontFamily = Roboto,
129 | fontWeight = FontWeight.Medium,
130 | letterSpacing = 0.15000000596046448.sp,
131 | lineHeight = 24.sp,
132 | fontSize = 16.sp
133 | ),
134 | titleSmall = TextStyle(
135 | fontFamily = Roboto,
136 | fontWeight = FontWeight.Medium,
137 | letterSpacing = 0.10000000149011612.sp,
138 | lineHeight = 20.sp,
139 | fontSize = 14.sp
140 | ),
141 | )
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * build.gradle Created by Yamin Siahmargooei at 2022/6/16
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | plugins {
22 | id 'com.android.application'
23 | id 'org.jetbrains.kotlin.android'
24 | id 'kotlin-parcelize'
25 | id 'com.google.devtools.ksp' version '1.8.22-1.0.11'
26 | }
27 |
28 | android {
29 | namespace 'io.github.yamin8000.owl'
30 | compileSdk 33
31 |
32 | defaultConfig {
33 | applicationId "io.github.yamin8000.owl"
34 | minSdk 21
35 | targetSdk 33
36 | versionCode 19
37 | versionName "1.3.6"
38 | vectorDrawables {
39 | useSupportLibrary true
40 | }
41 | ksp {
42 | arg("room.schemaLocation", "$projectDir/schemas".toString())
43 | }
44 | buildConfigField("String", "OWLBOT_TOKEN", "\"${System.getenv("OWLBOT_TOKEN")}\"")
45 | buildConfigField("String", "API_NINJA_KEY", "\"${System.getenv("API_NINJA_KEY")}\"")
46 | }
47 |
48 | flavorDimensions "version"
49 | productFlavors {
50 | irMarket {
51 | dimension "version"
52 | }
53 |
54 | free {
55 | dimension "version"
56 | }
57 | }
58 |
59 | buildTypes {
60 | release {
61 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
62 | minifyEnabled true
63 | shrinkResources true
64 | }
65 |
66 | debug {
67 | minifyEnabled false
68 | shrinkResources false
69 | }
70 | }
71 |
72 | compileOptions {
73 | sourceCompatibility JavaVersion.VERSION_17
74 | targetCompatibility JavaVersion.VERSION_17
75 | }
76 |
77 | kotlinOptions {
78 | jvmTarget = '17'
79 | languageVersion = '1.9'
80 | }
81 |
82 | buildFeatures {
83 | compose true
84 | buildConfig true
85 | }
86 |
87 | composeOptions {
88 | kotlinCompilerExtensionVersion compose_compiler_version
89 | }
90 |
91 | packagingOptions {
92 | resources {
93 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
94 | }
95 | }
96 |
97 | kotlin.sourceSets.all {
98 | languageSettings.enableLanguageFeature("DataObjects")
99 | }
100 | }
101 |
102 | dependencies {
103 | //core android
104 | implementation("androidx.core:core-ktx:1.10.1")
105 | implementation("androidx.palette:palette:1.0.0")
106 | //compose
107 | def material3_version = "1.1.1"
108 | implementation("androidx.compose.ui:ui:$compose_ui_libs_version")
109 | implementation("androidx.compose.material:material:$compose_libs_version")
110 | implementation("androidx.compose.ui:ui-tooling-preview:$compose_ui_libs_version")
111 | debugImplementation("androidx.compose.ui:ui-tooling:$compose_ui_libs_version")
112 | implementation("androidx.activity:activity-compose:1.7.2")
113 | implementation("androidx.compose.material:material-icons-extended:$compose_libs_version")
114 | implementation("androidx.compose.material3:material3:$material3_version")
115 | implementation("androidx.compose.material3:material3-window-size-class:$material3_version")
116 | //network
117 | def retrofit_version = "2.9.0"
118 | implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
119 | implementation("com.squareup.retrofit2:converter-moshi:$retrofit_version")
120 | //coil
121 | def coil_version = "2.4.0"
122 | implementation("io.coil-kt:coil:$coil_version")
123 | implementation("io.coil-kt:coil-compose:$coil_version")
124 | //navigation
125 | def nav_version = "2.6.0"
126 | implementation("androidx.navigation:navigation-compose:$nav_version")
127 | //datastore
128 | implementation("androidx.datastore:datastore-preferences:1.0.0")
129 | //room
130 | def room_version = "2.5.2"
131 | implementation("androidx.room:room-runtime:$room_version")
132 | annotationProcessor("androidx.room:room-compiler:$room_version")
133 | ksp("androidx.room:room-compiler:$room_version")
134 | implementation("androidx.room:room-ktx:$room_version")
135 | //lottie
136 | implementation("com.airbnb.android:lottie-compose:6.0.1")
137 | //ad
138 | irMarketImplementation("ir.tapsell.plus:tapsell-plus-sdk-android:2.1.8")
139 | }
--------------------------------------------------------------------------------
/app/src/irMarket/java/io/github/yamin8000/owl/content/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl/Owl.app.main
3 | * MainActivity.kt Copyrighted by Yamin Siahmargooei at 2023/4/22
4 | * MainActivity.kt Last modified at 2023/4/22
5 | * This file is part of Owl/Owl.app.main.
6 | * Copyright (C) 2023 Yamin Siahmargooei
7 | *
8 | * Owl/Owl.app.main is free software: you can redistribute it and/or modify
9 | * it under the terms of the GNU General Public License as published by
10 | * the Free Software Foundation, either version 3 of the License, or
11 | * (at your option) any later version.
12 | *
13 | * Owl/Owl.app.main is distributed in the hope that it will be useful,
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | * GNU General Public License for more details.
17 | *
18 | * You should have received a copy of the GNU General Public License
19 | * along with Owl. If not, see .
20 | */
21 |
22 | package io.github.yamin8000.owl.content
23 |
24 | import android.annotation.SuppressLint
25 | import android.os.Bundle
26 | import android.view.ViewGroup
27 | import androidx.activity.compose.setContent
28 | import androidx.compose.foundation.layout.Column
29 | import androidx.compose.foundation.layout.fillMaxWidth
30 | import androidx.compose.foundation.layout.padding
31 | import androidx.compose.foundation.layout.wrapContentHeight
32 | import androidx.compose.material3.ExperimentalMaterial3Api
33 | import androidx.compose.material3.Scaffold
34 | import androidx.compose.runtime.Composable
35 | import androidx.compose.runtime.LaunchedEffect
36 | import androidx.compose.runtime.getValue
37 | import androidx.compose.runtime.mutableStateOf
38 | import androidx.compose.runtime.remember
39 | import androidx.compose.runtime.setValue
40 | import androidx.compose.ui.Modifier
41 | import androidx.compose.ui.unit.dp
42 | import androidx.navigation.compose.NavHost
43 | import androidx.navigation.compose.rememberNavController
44 | import androidx.navigation.createGraph
45 | import io.github.yamin8000.owl.ad.AdConstants
46 | import io.github.yamin8000.owl.ad.AdHelper
47 | import io.github.yamin8000.owl.ad.TapsellAdContent
48 | import io.github.yamin8000.owl.content.settings.ThemeSetting
49 | import io.github.yamin8000.owl.util.log
50 | import ir.tapsell.plus.TapsellPlus
51 | import ir.tapsell.plus.TapsellPlusInitListener
52 | import ir.tapsell.plus.model.AdNetworkError
53 | import ir.tapsell.plus.model.AdNetworks
54 |
55 | class MainActivity : CommonActivity() {
56 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
57 | @ExperimentalMaterial3Api
58 | override fun onCreate(savedInstanceState: Bundle?) {
59 | super.onCreate(savedInstanceState)
60 |
61 | initTapsellAd()
62 | setContent {
63 | var currentTheme by remember { mutableStateOf(theme) }
64 | MainContent(
65 | currentTheme = currentTheme,
66 | content = {
67 | var adView by remember { mutableStateOf(null) }
68 | var adId: String by remember { mutableStateOf("") }
69 |
70 | LaunchedEffect(Unit) {
71 | adId = AdHelper.requestTapsellAd(this@MainActivity)
72 | AdHelper.showTapsellAd(this@MainActivity, adId, adView)
73 | }
74 | Scaffold {
75 | AdMainContent(
76 | onCreated = { adView = it },
77 | onUpdate = { adView = it },
78 | onThemeChanged = { currentTheme = it }
79 | )
80 | }
81 | }
82 | )
83 | }
84 | }
85 |
86 | private fun initTapsellAd() {
87 | TapsellPlus.initialize(this, AdConstants.TAPSELL_KEY, object : TapsellPlusInitListener {
88 | override fun onInitializeSuccess(ads: AdNetworks?) {
89 | log(ads?.name ?: "Unknown ad name")
90 | }
91 |
92 | override fun onInitializeFailed(ads: AdNetworks?, error: AdNetworkError?) {
93 | log(error?.errorMessage ?: "Unknown tapsell init error")
94 | }
95 | })
96 | }
97 |
98 | @Composable
99 | private fun AdMainContent(
100 | onCreated: (ViewGroup) -> Unit,
101 | onUpdate: (ViewGroup) -> Unit,
102 | onThemeChanged: (ThemeSetting) -> Unit
103 | ) {
104 | Column {
105 | val navController = rememberNavController()
106 | val builder = mainNavigationGraph(
107 | navController = navController,
108 | outsideInput = outsideInput,
109 | onThemeChanged = onThemeChanged
110 | )
111 | NavHost(
112 | modifier = Modifier.weight(1f),
113 | navController = navController,
114 | graph = remember(startDestination, route, builder) {
115 | navController.createGraph(startDestination, route, builder)
116 | }
117 | )
118 | TapsellAdContent(
119 | modifier = Modifier
120 | .wrapContentHeight()
121 | .padding(4.dp)
122 | .fillMaxWidth(),
123 | onCreated = onCreated,
124 | onUpdate = onUpdate
125 | )
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/About.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * About.kt Created by Yamin Siahmargooei at 2022/9/19
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content
22 |
23 | import androidx.compose.foundation.Image
24 | import androidx.compose.foundation.layout.*
25 | import androidx.compose.foundation.rememberScrollState
26 | import androidx.compose.foundation.verticalScroll
27 | import androidx.compose.material3.ExperimentalMaterial3Api
28 | import androidx.compose.material3.MaterialTheme
29 | import androidx.compose.material3.Text
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.ui.Alignment
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.graphics.ColorFilter
34 | import androidx.compose.ui.layout.ContentScale
35 | import androidx.compose.ui.platform.LocalUriHandler
36 | import androidx.compose.ui.res.painterResource
37 | import androidx.compose.ui.res.stringResource
38 | import androidx.compose.ui.text.style.TextDecoration
39 | import androidx.compose.ui.unit.dp
40 | import io.github.yamin8000.owl.BuildConfig
41 | import io.github.yamin8000.owl.R
42 | import io.github.yamin8000.owl.ui.composable.PersianText
43 | import io.github.yamin8000.owl.ui.composable.Ripple
44 | import io.github.yamin8000.owl.ui.composable.ScaffoldWithTitle
45 |
46 | @OptIn(ExperimentalMaterial3Api::class)
47 | @Composable
48 | fun AboutContent(
49 | onBackClick: () -> Unit
50 | ) {
51 | ScaffoldWithTitle(
52 | title = stringResource(R.string.about),
53 | onBackClick = onBackClick,
54 | content = {
55 | Column(
56 | modifier = Modifier.verticalScroll(rememberScrollState()),
57 | verticalArrangement = Arrangement.spacedBy(8.dp),
58 | content = {
59 | val uriHandler = LocalUriHandler.current
60 | val sourceUri = stringResource(R.string.github_source)
61 | val owlBotUri = stringResource(R.string.owl_bot_link)
62 | val licenseUri = stringResource(R.string.license_link)
63 | Ripple(
64 | onClick = { uriHandler.openUri(licenseUri) },
65 | content = {
66 | Image(
67 | painterResource(id = R.drawable.ic_gplv3),
68 | stringResource(id = R.string.gplv3_image_description),
69 | modifier = Modifier
70 | .padding(32.dp)
71 | .fillMaxWidth(),
72 | contentScale = ContentScale.FillWidth,
73 | colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
74 | )
75 | }
76 | )
77 | Row(
78 | modifier = Modifier.fillMaxWidth(),
79 | verticalAlignment = Alignment.CenterVertically,
80 | horizontalArrangement = Arrangement.spacedBy(
81 | 8.dp,
82 | Alignment.CenterHorizontally
83 | ),
84 | content = {
85 | PersianText(stringResource(R.string.version_name))
86 | PersianText(BuildConfig.VERSION_NAME)
87 | }
88 | )
89 | PersianText(
90 | text = stringResource(id = R.string.license_header),
91 | modifier = Modifier.fillMaxWidth()
92 | )
93 | Ripple(
94 | onClick = { uriHandler.openUri(sourceUri) },
95 | content = {
96 | Text(
97 | text = sourceUri,
98 | textDecoration = TextDecoration.Underline
99 | )
100 | }
101 | )
102 | PersianText(
103 | text = stringResource(id = R.string.about_app),
104 | modifier = Modifier.fillMaxWidth()
105 | )
106 | Ripple(
107 | onClick = { uriHandler.openUri(owlBotUri) },
108 | content = {
109 | Text(
110 | text = owlBotUri,
111 | textDecoration = TextDecoration.Underline
112 | )
113 | }
114 | )
115 | }
116 | )
117 | }
118 | )
119 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/util/RecomposeHighlighter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * RecomposeHighlighter.kt Created by Yamin Siahmargooei at 2022/10/15
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.util
22 |
23 | import androidx.compose.runtime.LaunchedEffect
24 | import androidx.compose.runtime.Stable
25 | import androidx.compose.runtime.mutableStateOf
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.composed
29 | import androidx.compose.ui.draw.drawWithCache
30 | import androidx.compose.ui.geometry.Offset
31 | import androidx.compose.ui.geometry.Size
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.graphics.SolidColor
34 | import androidx.compose.ui.graphics.drawscope.Fill
35 | import androidx.compose.ui.graphics.drawscope.Stroke
36 | import androidx.compose.ui.graphics.lerp
37 | import androidx.compose.ui.platform.debugInspectorInfo
38 | import androidx.compose.ui.unit.dp
39 | import kotlinx.coroutines.delay
40 | import kotlin.math.min
41 |
42 | /**
43 | * A [Modifier] that draws a border around elements that are recomposing. The border increases in
44 | * size and interpolates from red to green as more recompositions occur before a timeout.
45 | */
46 | @Stable
47 | fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
48 |
49 | // Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
50 | // Modifier.composed will still remember unique data per call site.
51 | private val recomposeModifier =
52 | Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
53 | // The total number of compositions that have occurred. We're not using a State<> here be
54 | // able to read/write the value without invalidating (which would cause infinite
55 | // recomposition).
56 | val totalCompositions = remember { arrayOf(0L) }
57 | totalCompositions[0]++
58 |
59 | // The value of totalCompositions at the last timeout.
60 | val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
61 |
62 | // Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions
63 | // as the key is really just to cause the timer to restart every composition).
64 | LaunchedEffect(totalCompositions[0]) {
65 | delay(3000)
66 | totalCompositionsAtLastTimeout.value = totalCompositions[0]
67 | }
68 |
69 | Modifier.drawWithCache {
70 | onDrawWithContent {
71 | // Draw actual content.
72 | drawContent()
73 |
74 | // Below is to draw the highlight, if necessary. A lot of the logic is copied from
75 | // Modifier.border
76 | val numCompositionsSinceTimeout =
77 | totalCompositions[0] - totalCompositionsAtLastTimeout.value
78 |
79 | val hasValidBorderParams = size.minDimension > 0f
80 | if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
81 | return@onDrawWithContent
82 | }
83 |
84 | val (color, strokeWidthPx) =
85 | when (numCompositionsSinceTimeout) {
86 | // We need at least one composition to draw, so draw the smallest border
87 | // color in blue.
88 | 1L -> Color.Blue to 1f
89 | // 2 compositions is _probably_ okay.
90 | 2L -> Color.Green to 2.dp.toPx()
91 | // 3 or more compositions before timeout may indicate an issue. lerp the
92 | // color from yellow to red, and continually increase the border size.
93 | else -> {
94 | lerp(
95 | Color.Yellow.copy(alpha = 0.8f),
96 | Color.Red.copy(alpha = 0.5f),
97 | min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
98 | ) to numCompositionsSinceTimeout.toInt().dp.toPx()
99 | }
100 | }
101 |
102 | val halfStroke = strokeWidthPx / 2
103 | val topLeft = Offset(halfStroke, halfStroke)
104 | val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
105 |
106 | val fillArea = (strokeWidthPx * 2) > size.minDimension
107 | val rectTopLeft = if (fillArea) Offset.Zero else topLeft
108 | val size = if (fillArea) size else borderSize
109 | val style = if (fillArea) Fill else Stroke(strokeWidthPx)
110 |
111 | drawRect(
112 | brush = SolidColor(color),
113 | topLeft = rectTopLeft,
114 | size = size,
115 | style = style
116 | )
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Theme.kt Created by Yamin Siahmargooei at 2022/6/16
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.ui.theme
22 |
23 | import android.app.Activity
24 | import android.os.Build
25 | import androidx.compose.foundation.isSystemInDarkTheme
26 | import androidx.compose.material3.*
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.SideEffect
29 | import androidx.compose.ui.graphics.toArgb
30 | import androidx.compose.ui.platform.LocalContext
31 | import androidx.compose.ui.platform.LocalView
32 | import androidx.core.view.WindowCompat
33 |
34 | private val LightColors = lightColorScheme(
35 | primary = md_theme_light_primary,
36 | onPrimary = md_theme_light_onPrimary,
37 | primaryContainer = md_theme_light_primaryContainer,
38 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
39 | inversePrimary = md_theme_light_inversePrimary,
40 | secondary = md_theme_light_secondary,
41 | onSecondary = md_theme_light_onSecondary,
42 | secondaryContainer = md_theme_light_secondaryContainer,
43 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
44 | tertiary = md_theme_light_tertiary,
45 | onTertiary = md_theme_light_onTertiary,
46 | tertiaryContainer = md_theme_light_tertiaryContainer,
47 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
48 | background = md_theme_light_background,
49 | onBackground = md_theme_light_onBackground,
50 | surface = md_theme_light_surface,
51 | onSurface = md_theme_light_onSurface,
52 | surfaceVariant = md_theme_light_surfaceVariant,
53 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
54 | surfaceTint = md_theme_light_surfaceTint,
55 | inverseSurface = md_theme_light_inverseSurface,
56 | inverseOnSurface = md_theme_light_inverseOnSurface,
57 | error = md_theme_light_error,
58 | onError = md_theme_light_onError,
59 | errorContainer = md_theme_light_errorContainer,
60 | onErrorContainer = md_theme_light_onErrorContainer,
61 | outline = md_theme_light_outline,
62 | //surfaceTintColor = md_theme_light_surfaceTintColor,
63 | )
64 |
65 |
66 | private val DarkColors = darkColorScheme(
67 | primary = md_theme_dark_primary,
68 | onPrimary = md_theme_dark_onPrimary,
69 | primaryContainer = md_theme_dark_primaryContainer,
70 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
71 | secondary = md_theme_dark_secondary,
72 | onSecondary = md_theme_dark_onSecondary,
73 | secondaryContainer = md_theme_dark_secondaryContainer,
74 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
75 | tertiary = md_theme_dark_tertiary,
76 | onTertiary = md_theme_dark_onTertiary,
77 | tertiaryContainer = md_theme_dark_tertiaryContainer,
78 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
79 | error = md_theme_dark_error,
80 | errorContainer = md_theme_dark_errorContainer,
81 | onError = md_theme_dark_onError,
82 | onErrorContainer = md_theme_dark_onErrorContainer,
83 | background = md_theme_dark_background,
84 | onBackground = md_theme_dark_onBackground,
85 | surface = md_theme_dark_surface,
86 | onSurface = md_theme_dark_onSurface,
87 | surfaceVariant = md_theme_dark_surfaceVariant,
88 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
89 | outline = md_theme_dark_outline,
90 | inverseOnSurface = md_theme_dark_inverseOnSurface,
91 | inverseSurface = md_theme_dark_inverseSurface,
92 | inversePrimary = md_theme_dark_inversePrimary,
93 | surfaceTint = md_theme_dark_surfaceTint,
94 | //surfaceTintColor = md_theme_dark_surfaceTintColor,
95 | )
96 |
97 | @Composable
98 | fun OwlTheme(
99 | isDarkTheme: Boolean = isSystemInDarkTheme(),
100 | isPreviewing: Boolean = false,
101 | isDynamicColor: Boolean,
102 | content: @Composable () -> Unit
103 | ) {
104 | val isDynamicColorReadyDevice = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
105 |
106 | val colors = when {
107 | isDynamicColorReadyDevice && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current)
108 | isDynamicColorReadyDevice && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current)
109 | isDarkTheme -> DarkColors
110 | else -> LightColors
111 | }
112 |
113 | if (!isPreviewing) {
114 | val activity = LocalView.current.context as Activity
115 | SideEffect {
116 | activity.window.statusBarColor = colors.surface.toArgb()
117 | activity.window.navigationBarColor = colors.surface.toArgb()
118 | val wic = WindowCompat.getInsetsController(activity.window, activity.window.decorView)
119 | wic.isAppearanceLightStatusBars = !isDarkTheme
120 | wic.isAppearanceLightNavigationBars = !isDarkTheme
121 | }
122 | }
123 |
124 | MaterialTheme(
125 | colorScheme = colors,
126 | typography = AppTypography,
127 | content = content
128 | )
129 | }
130 |
131 | @Suppress("unused")
132 | @Composable
133 | fun PreviewTheme(
134 | isDarkTheme: Boolean = isSystemInDarkTheme(),
135 | content: @Composable () -> Unit
136 | ) {
137 | OwlTheme(
138 | isDarkTheme = isDarkTheme,
139 | isPreviewing = true,
140 | isDynamicColor = false,
141 | content = content
142 | )
143 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/CommonActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl2/Owl2.app.main
3 | * MainActivity.kt Copyrighted by Yamin Siahmargooei at 2023/5/18
4 | * MainActivity.kt Last modified at 2023/5/18
5 | * This file is part of Owl2/Owl2.app.main.
6 | * Copyright (C) 2023 Yamin Siahmargooei
7 | *
8 | * Owl2/Owl2.app.main is free software: you can redistribute it and/or modify
9 | * it under the terms of the GNU General Public License as published by
10 | * the Free Software Foundation, either version 3 of the License, or
11 | * (at your option) any later version.
12 | *
13 | * Owl2/Owl2.app.main is distributed in the hope that it will be useful,
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | * GNU General Public License for more details.
17 | *
18 | * You should have received a copy of the GNU General Public License
19 | * along with Owl2. If not, see .
20 | */
21 |
22 | package io.github.yamin8000.owl.content
23 |
24 | import android.content.Intent
25 | import android.os.Build
26 | import android.os.Bundle
27 | import androidx.activity.ComponentActivity
28 | import androidx.compose.foundation.isSystemInDarkTheme
29 | import androidx.compose.runtime.Composable
30 | import androidx.navigation.NavGraphBuilder
31 | import androidx.navigation.NavHostController
32 | import androidx.navigation.compose.composable
33 | import androidx.room.Room
34 | import io.github.yamin8000.owl.content.favourites.FavouritesContent
35 | import io.github.yamin8000.owl.content.history.HistoryContent
36 | import io.github.yamin8000.owl.content.home.HomeContent
37 | import io.github.yamin8000.owl.content.settings.SettingsContent
38 | import io.github.yamin8000.owl.content.settings.ThemeSetting
39 | import io.github.yamin8000.owl.db.AppDatabase
40 | import io.github.yamin8000.owl.ui.navigation.Nav
41 | import io.github.yamin8000.owl.ui.theme.OwlTheme
42 | import io.github.yamin8000.owl.util.Constants
43 | import io.github.yamin8000.owl.util.DataStoreHelper
44 | import io.github.yamin8000.owl.util.log
45 | import kotlinx.coroutines.runBlocking
46 |
47 | abstract class CommonActivity : ComponentActivity() {
48 |
49 | protected var outsideInput: String? = null
50 |
51 | protected var theme: ThemeSetting = ThemeSetting.System
52 |
53 | protected val startDestination = "${Nav.Routes.Home}/{${Nav.Arguments.Search}}"
54 |
55 | protected val route: String? = null
56 |
57 | override fun onCreate(savedInstanceState: Bundle?) {
58 | super.onCreate(savedInstanceState)
59 |
60 | Constants.db = createDb()
61 | outsideInput = handleOutsideInputIntent()
62 | try {
63 | runBlocking { theme = getCurrentTheme() }
64 | } catch (e: InterruptedException) {
65 | log(e.stackTraceToString())
66 | }
67 | }
68 |
69 | @Composable
70 | protected fun MainContent(
71 | currentTheme: ThemeSetting,
72 | content: @Composable () -> Unit
73 | ) {
74 | OwlTheme(
75 | isDarkTheme = isDarkTheme(currentTheme, isSystemInDarkTheme()),
76 | isDynamicColor = currentTheme == ThemeSetting.System,
77 | content = content
78 | )
79 | }
80 |
81 | protected fun mainNavigationGraph(
82 | navController: NavHostController,
83 | outsideInput: String?,
84 | onThemeChanged: (ThemeSetting) -> Unit
85 | ): NavGraphBuilder.() -> Unit = {
86 | composable("${Nav.Routes.Home}/{${Nav.Arguments.Search}}") {
87 | var searchTerm = it.arguments?.getString(Nav.Arguments.Search.toString())
88 | if (searchTerm == null && outsideInput != null)
89 | searchTerm = outsideInput.toString()
90 | HomeContent(
91 | searchTerm = searchTerm,
92 | onFavouritesClick = { navController.navigate(Nav.Routes.Favourites.toString()) },
93 | onHistoryClick = { navController.navigate(Nav.Routes.History.toString()) },
94 | onInfoClick = { navController.navigate(Nav.Routes.About.toString()) },
95 | onSettingsClick = { navController.navigate(Nav.Routes.Settings.toString()) }
96 | )
97 | }
98 |
99 | composable(Nav.Routes.About.toString()) {
100 | AboutContent { navController.popBackStack() }
101 | }
102 |
103 | composable(Nav.Routes.Favourites.toString()) {
104 | FavouritesContent(
105 | onFavouritesItemClick = { favourite -> navController.navigate("${Nav.Routes.Home}/${favourite}") },
106 | onBackClick = { navController.popBackStack() }
107 | )
108 | }
109 |
110 | composable(Nav.Routes.History.toString()) {
111 | HistoryContent(
112 | onHistoryItemClick = { history -> navController.navigate("${Nav.Routes.Home}/${history}") },
113 | onBackClick = { navController.popBackStack() }
114 | )
115 | }
116 |
117 | composable(Nav.Routes.Settings.toString()) {
118 | SettingsContent(
119 | onThemeChanged = onThemeChanged,
120 | onBackClick = { navController.popBackStack() }
121 | )
122 | }
123 | }
124 |
125 | private fun isDarkTheme(
126 | themeSetting: ThemeSetting,
127 | isSystemInDarkTheme: Boolean
128 | ): Boolean {
129 | if (themeSetting == ThemeSetting.Light) return false
130 | if (themeSetting == ThemeSetting.System) return isSystemInDarkTheme
131 | if (themeSetting == ThemeSetting.Dark) return true
132 | return false
133 | }
134 |
135 | private fun createDb() = Room.databaseBuilder(
136 | this,
137 | AppDatabase::class.java,
138 | "db"
139 | ).build()
140 |
141 | private fun handleOutsideInputIntent(): String? {
142 | return if (intent.type == "text/plain") {
143 | when (intent.action) {
144 | Intent.ACTION_TRANSLATE, Intent.ACTION_DEFINE, Intent.ACTION_SEND -> {
145 | intent.getStringExtra(Intent.EXTRA_TEXT)
146 | }
147 |
148 | Intent.ACTION_PROCESS_TEXT -> {
149 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
150 | intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)
151 | else null
152 | }
153 |
154 | else -> null
155 | }
156 | } else null
157 | }
158 |
159 | private suspend fun getCurrentTheme() = ThemeSetting.valueOf(
160 | DataStoreHelper(settingsDataStore).getString(Constants.THEME) ?: ThemeSetting.System.name
161 | )
162 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
26 |
29 |
34 |
39 |
44 |
49 |
54 |
59 |
64 |
69 |
74 |
79 |
84 |
89 |
94 |
99 |
104 |
109 |
114 |
119 |
124 |
129 |
134 |
139 |
144 |
149 |
154 |
159 |
164 |
169 |
174 |
179 |
184 |
189 |
190 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/ui/util/DynamicColorPalette.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * DynamicColorPalette.kt Created by Yamin Siahmargooei at 2022/9/26
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.ui.util
22 |
23 | import android.content.Context
24 | import androidx.collection.LruCache
25 | import androidx.compose.animation.animateColorAsState
26 | import androidx.compose.animation.core.Spring
27 | import androidx.compose.animation.core.spring
28 | import androidx.compose.material3.MaterialTheme
29 | import androidx.compose.runtime.*
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.platform.LocalContext
32 | import androidx.core.graphics.drawable.toBitmap
33 | import androidx.palette.graphics.Palette
34 | import coil.imageLoader
35 | import coil.request.ImageRequest
36 | import coil.request.SuccessResult
37 | import coil.size.Scale
38 | import kotlinx.coroutines.Dispatchers
39 | import kotlinx.coroutines.withContext
40 |
41 | @Composable
42 | fun rememberDominantColorState(
43 | context: Context = LocalContext.current,
44 | defaultColor: Color = MaterialTheme.colorScheme.surfaceVariant,
45 | defaultOnColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
46 | cacheSize: Int = 12,
47 | isColorValid: (Color) -> Boolean = { true }
48 | ): DominantColorState = remember {
49 | DominantColorState(context, defaultColor, defaultOnColor, cacheSize, isColorValid)
50 | }
51 |
52 | /**
53 | * A composable which allows dynamic theming of the [androidx.compose.material.Colors.primary]
54 | * color from an image.
55 | */
56 | @Composable
57 | fun DynamicThemePrimaryColorsFromImage(
58 | dominantColorState: DominantColorState = rememberDominantColorState(),
59 | content: @Composable () -> Unit
60 | ) {
61 | val colors = MaterialTheme.colorScheme.copy(
62 | primary = animateColorAsState(
63 | dominantColorState.color,
64 | spring(stiffness = Spring.StiffnessLow)
65 | ).value,
66 | onPrimary = animateColorAsState(
67 | dominantColorState.onColor,
68 | spring(stiffness = Spring.StiffnessLow)
69 | ).value
70 | )
71 | MaterialTheme(colorScheme = colors, content = content)
72 | }
73 |
74 | /**
75 | * A class which stores and caches the result of any calculated dominant colors
76 | * from images.
77 | *
78 | * @param context Android context
79 | * @param defaultColor The default color, which will be used if [calculateDominantColor] fails to
80 | * calculate a dominant color
81 | * @param defaultOnColor The default foreground 'on color' for [defaultColor].
82 | * @param cacheSize The size of the [LruCache] used to store recent results. Pass `0` to
83 | * disable the cache.
84 | * @param isColorValid A lambda which allows filtering of the calculated image colors.
85 | */
86 | @Stable
87 | class DominantColorState(
88 | private val context: Context,
89 | private val defaultColor: Color,
90 | private val defaultOnColor: Color,
91 | cacheSize: Int = 12,
92 | private val isColorValid: (Color) -> Boolean = { true }
93 | ) {
94 | var color by mutableStateOf(defaultColor)
95 | private set
96 | var onColor by mutableStateOf(defaultOnColor)
97 | private set
98 |
99 | private val cache = when {
100 | cacheSize > 0 -> LruCache(cacheSize)
101 | else -> null
102 | }
103 |
104 | suspend fun updateColorsFromImageUrl(url: String) {
105 | val result = calculateDominantColor(url)
106 | color = result?.color ?: defaultColor
107 | onColor = result?.onColor ?: defaultOnColor
108 | }
109 |
110 | private suspend fun calculateDominantColor(url: String): DominantColors? {
111 | val cached = cache?.get(url)
112 | if (cached != null) {
113 | // If we already have the result cached, return early now...
114 | return cached
115 | }
116 |
117 | // Otherwise we calculate the swatches in the image, and return the first valid color
118 | return calculateSwatchesInImage(context, url)
119 | .filter { swatch ->
120 | val lightness = swatch.hsl[2]
121 | val saturation = swatch.hsl[1]
122 | lightness > .1f && lightness <= .9f && saturation >= .1f
123 | }
124 | // First we want to sort the list by the color's population
125 | .sortedByDescending { swatch -> swatch.population }
126 | // Then we want to find the first valid color
127 | .firstOrNull { swatch -> isColorValid(Color(swatch.rgb)) }
128 | // If we found a valid swatch, wrap it in a [DominantColors]
129 | ?.let { swatch ->
130 | DominantColors(
131 | color = Color(swatch.rgb),
132 | onColor = Color(swatch.bodyTextColor).copy(alpha = 1f)
133 | )
134 | }
135 | // Cache the resulting [DominantColors]
136 | ?.also { result -> cache?.put(url, result) }
137 | }
138 |
139 | /**
140 | * Reset the color values to [defaultColor].
141 | */
142 | fun reset() {
143 | color = defaultColor
144 | onColor = defaultOnColor
145 | }
146 | }
147 |
148 | @Immutable
149 | private data class DominantColors(val color: Color, val onColor: Color)
150 |
151 | /**
152 | * Fetches the given [imageUrl] with Coil, then uses [Palette] to calculate the dominant color.
153 | */
154 | private suspend fun calculateSwatchesInImage(
155 | context: Context,
156 | imageUrl: String
157 | ): List {
158 | val request = ImageRequest.Builder(context)
159 | .data(imageUrl)
160 | // We scale the image to cover 128px x 128px (i.e. min dimension == 128px)
161 | .size(128).scale(Scale.FILL)
162 | // Disable hardware bitmaps, since Palette uses Bitmap.getPixels()
163 | .allowHardware(false)
164 | // Set a custom memory cache key to avoid overwriting the displayed image in the cache
165 | .memoryCacheKey("$imageUrl.palette")
166 | .build()
167 |
168 | val bitmap = when (val result = context.imageLoader.execute(request)) {
169 | is SuccessResult -> result.drawable.toBitmap()
170 | else -> null
171 | }
172 |
173 | return bitmap?.let {
174 | withContext(Dispatchers.Default) {
175 | val palette = Palette.Builder(bitmap)
176 | // Disable any bitmap resizing in Palette. We've already loaded an appropriately
177 | // sized bitmap through Coil
178 | .resizeBitmapArea(0)
179 | // Clear any built-in filters. We want the unfiltered dominant color
180 | .clearFilters()
181 | // We reduce the maximum color count down to 8
182 | .maximumColorCount(8)
183 | .generate()
184 |
185 | palette.swatches
186 | }
187 | } ?: emptyList()
188 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/MainBottomBar.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * MainBottomBar.kt Created by Yamin Siahmargooei at 2022/9/19
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content
22 |
23 | import android.content.Context
24 | import androidx.compose.animation.Crossfade
25 | import androidx.compose.foundation.layout.Arrangement
26 | import androidx.compose.foundation.layout.Column
27 | import androidx.compose.foundation.layout.fillMaxWidth
28 | import androidx.compose.foundation.layout.padding
29 | import androidx.compose.foundation.lazy.LazyRow
30 | import androidx.compose.foundation.lazy.items
31 | import androidx.compose.foundation.shape.CutCornerShape
32 | import androidx.compose.foundation.text.KeyboardActions
33 | import androidx.compose.foundation.text.KeyboardOptions
34 | import androidx.compose.material.icons.Icons
35 | import androidx.compose.material.icons.twotone.Clear
36 | import androidx.compose.material.icons.twotone.Search
37 | import androidx.compose.material.icons.twotone.Stop
38 | import androidx.compose.material3.*
39 | import androidx.compose.runtime.*
40 | import androidx.compose.ui.Modifier
41 | import androidx.compose.ui.platform.LocalContext
42 | import androidx.compose.ui.res.stringResource
43 | import androidx.compose.ui.text.TextStyle
44 | import androidx.compose.ui.text.input.ImeAction
45 | import androidx.compose.ui.text.input.KeyboardCapitalization
46 | import androidx.compose.ui.text.input.KeyboardType
47 | import androidx.compose.ui.text.style.TextAlign
48 | import androidx.compose.ui.text.style.TextDirection
49 | import androidx.compose.ui.unit.dp
50 | import androidx.compose.ui.unit.sp
51 | import io.github.yamin8000.owl.R
52 | import io.github.yamin8000.owl.ui.composable.ClickableIcon
53 | import io.github.yamin8000.owl.ui.composable.HighlightText
54 | import io.github.yamin8000.owl.ui.composable.PersianText
55 | import io.github.yamin8000.owl.ui.theme.Samim
56 | import io.github.yamin8000.owl.util.ImmutableHolder
57 | import io.github.yamin8000.owl.util.getCurrentLocale
58 | import kotlinx.coroutines.delay
59 | import java.util.*
60 |
61 | @OptIn(ExperimentalMaterial3Api::class)
62 | @Composable
63 | fun MainBottomBar(
64 | searchTerm: String?,
65 | suggestions: ImmutableHolder>,
66 | onSuggestionClick: (String) -> Unit,
67 | isSearching: Boolean,
68 | onSearchTermChanged: (String) -> Unit,
69 | onSearch: (String) -> Unit,
70 | onCancel: () -> Unit
71 | ) {
72 | var searchText by remember { mutableStateOf(searchTerm ?: "") }
73 | Column {
74 | if (suggestions.item.isNotEmpty()) {
75 | LazyRow(
76 | modifier = Modifier.padding(8.dp),
77 | horizontalArrangement = Arrangement.spacedBy(4.dp),
78 | content = {
79 | items(
80 | items = suggestions.item,
81 | itemContent = {
82 | ElevatedSuggestionChip(
83 | label = { HighlightText(it, searchText) },
84 | onClick = {
85 | onSuggestionClick(it)
86 | searchText = it
87 | }
88 | )
89 | }
90 | )
91 | }
92 | )
93 | }
94 | if (isSearching)
95 | RainbowLinearProgress()
96 | Crossfade(
97 | targetState = isSearching,
98 | content = { target ->
99 | if (target) {
100 | BottomAppBar(
101 | actions = {},
102 | floatingActionButton = {
103 | FloatingActionButton(
104 | onClick = onCancel,
105 | content = {
106 | Icon(
107 | imageVector = Icons.TwoTone.Stop,
108 | contentDescription = stringResource(R.string.cancel)
109 | )
110 | }
111 | )
112 | }
113 | )
114 | } else {
115 | BottomAppBar {
116 | TextField(
117 | singleLine = true,
118 | shape = CutCornerShape(topEnd = 10.dp, topStart = 10.dp),
119 | modifier = Modifier
120 | .fillMaxWidth()
121 | .padding(16.dp, 0.dp, 16.dp, 0.dp),
122 | label = {
123 | PersianText(
124 | stringResource(R.string.search),
125 | modifier = Modifier.fillMaxWidth()
126 | )
127 | },
128 | placeholder = {
129 | PersianText(
130 | text = stringResource(R.string.search_hint),
131 | modifier = Modifier.fillMaxWidth(),
132 | fontSize = 12.sp
133 | )
134 | },
135 | leadingIcon = {
136 | ClickableIcon(
137 | imageVector = Icons.TwoTone.Clear,
138 | contentDescription = stringResource(R.string.clear),
139 | onClick = { searchText = "" }
140 | )
141 | },
142 | trailingIcon = {
143 | ClickableIcon(
144 | imageVector = Icons.TwoTone.Search,
145 | contentDescription = stringResource(R.string.search),
146 | onClick = { onSearch(searchText) }
147 | )
148 | },
149 | value = searchText,
150 | onValueChange = {
151 | searchText = it
152 | onSearchTermChanged(searchText)
153 | },
154 | textStyle = getTextStyleBasedOnLocale(LocalContext.current),
155 | keyboardActions = KeyboardActions(onSearch = { onSearch(searchText) }),
156 | keyboardOptions = KeyboardOptions(
157 | imeAction = ImeAction.Search,
158 | keyboardType = KeyboardType.Text,
159 | capitalization = KeyboardCapitalization.Words
160 | )
161 | )
162 | }
163 | }
164 | }
165 | )
166 | }
167 | }
168 |
169 | @Composable
170 | private fun RainbowLinearProgress() {
171 | fun randomBeam(): Int = (16..255).random()
172 | val colors = buildList {
173 | repeat((5..10).random()) {
174 | add(androidx.compose.ui.graphics.Color(randomBeam(), randomBeam(), randomBeam()))
175 | }
176 | }
177 | var color by remember { mutableStateOf(colors.first()) }
178 | LaunchedEffect(Unit) {
179 | while (true) {
180 | color = colors.random()
181 | delay(250)
182 | }
183 | }
184 | LinearProgressIndicator(
185 | modifier = Modifier.fillMaxWidth(),
186 | color = color
187 | )
188 | }
189 |
190 | private fun getTextStyleBasedOnLocale(
191 | context: Context
192 | ): TextStyle {
193 | return if (getCurrentLocale(context).language == Locale("fa").language) {
194 | TextStyle(
195 | fontFamily = Samim,
196 | textAlign = TextAlign.Right,
197 | textDirection = TextDirection.Rtl
198 | )
199 | } else TextStyle()
200 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Use the maximum available, or set MAX_FD != -1 to use that value.
89 | MAX_FD=maximum
90 |
91 | warn () {
92 | echo "$*"
93 | } >&2
94 |
95 | die () {
96 | echo
97 | echo "$*"
98 | echo
99 | exit 1
100 | } >&2
101 |
102 | # OS specific support (must be 'true' or 'false').
103 | cygwin=false
104 | msys=false
105 | darwin=false
106 | nonstop=false
107 | case "$( uname )" in #(
108 | CYGWIN* ) cygwin=true ;; #(
109 | Darwin* ) darwin=true ;; #(
110 | MSYS* | MINGW* ) msys=true ;; #(
111 | NONSTOP* ) nonstop=true ;;
112 | esac
113 |
114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
115 |
116 |
117 | # Determine the Java command to use to start the JVM.
118 | if [ -n "$JAVA_HOME" ] ; then
119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
120 | # IBM's JDK on AIX uses strange locations for the executables
121 | JAVACMD=$JAVA_HOME/jre/sh/java
122 | else
123 | JAVACMD=$JAVA_HOME/bin/java
124 | fi
125 | if [ ! -x "$JAVACMD" ] ; then
126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
127 |
128 | Please set the JAVA_HOME variable in your environment to match the
129 | location of your Java installation."
130 | fi
131 | else
132 | JAVACMD=java
133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
134 |
135 | Please set the JAVA_HOME variable in your environment to match the
136 | location of your Java installation."
137 | fi
138 |
139 | # Increase the maximum file descriptors if we can.
140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
141 | case $MAX_FD in #(
142 | max*)
143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
144 | # shellcheck disable=SC3045
145 | MAX_FD=$( ulimit -H -n ) ||
146 | warn "Could not query maximum file descriptor limit"
147 | esac
148 | case $MAX_FD in #(
149 | '' | soft) :;; #(
150 | *)
151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
152 | # shellcheck disable=SC3045
153 | ulimit -n "$MAX_FD" ||
154 | warn "Could not set maximum file descriptor limit to $MAX_FD"
155 | esac
156 | fi
157 |
158 | # Collect all arguments for the java command, stacking in reverse order:
159 | # * args from the command line
160 | # * the main class name
161 | # * -classpath
162 | # * -D...appname settings
163 | # * --module-path (only if needed)
164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
165 |
166 | # For Cygwin or MSYS, switch paths to Windows format before running java
167 | if "$cygwin" || "$msys" ; then
168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
170 |
171 | JAVACMD=$( cygpath --unix "$JAVACMD" )
172 |
173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
174 | for arg do
175 | if
176 | case $arg in #(
177 | -*) false ;; # don't mess with options #(
178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
179 | [ -e "$t" ] ;; #(
180 | *) false ;;
181 | esac
182 | then
183 | arg=$( cygpath --path --ignore --mixed "$arg" )
184 | fi
185 | # Roll the args list around exactly as many times as the number of
186 | # args, so each arg winds up back in the position where it started, but
187 | # possibly modified.
188 | #
189 | # NB: a `for` loop captures its iteration list before it begins, so
190 | # changing the positional parameters here affects neither the number of
191 | # iterations, nor the values presented in `arg`.
192 | shift # remove old arg
193 | set -- "$@" "$arg" # push replacement arg
194 | done
195 | fi
196 |
197 |
198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
200 |
201 | # Collect all arguments for the java command;
202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
203 | # shell script including quotes and variable substitutions, so put them in
204 | # double quotes to make sure that they get re-expanded; and
205 | # * put everything else in single quotes, so that it's not re-expanded.
206 |
207 | set -- \
208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
209 | -classpath "$CLASSPATH" \
210 | org.gradle.wrapper.GradleWrapperMain \
211 | "$@"
212 |
213 | # Stop when "xargs" is not available.
214 | if ! command -v xargs >/dev/null 2>&1
215 | then
216 | die "xargs is not available"
217 | fi
218 |
219 | # Use "xargs" to parse quoted args.
220 | #
221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
222 | #
223 | # In Bash we could simply go:
224 | #
225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
226 | # set -- "${ARGS[@]}" "$@"
227 | #
228 | # but POSIX shell has neither arrays nor command substitution, so instead we
229 | # post-process each arg (as a line of input to sed) to backslash-escape any
230 | # character that might be a shell metacharacter, then use eval to reverse
231 | # that process (while maintaining the separation between arguments), and wrap
232 | # the whole thing up as a single "set" statement.
233 | #
234 | # This will of course break if any of these variables contains a newline or
235 | # an unmatched quote.
236 | #
237 |
238 | eval "set -- $(
239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
240 | xargs -n1 |
241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
242 | tr '\n' ' '
243 | )" '"$@"'
244 |
245 | exec "$JAVACMD" "$@"
246 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/home/Home.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Home.kt Created by Yamin Siahmargooei at 2022/8/22
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content.home
22 |
23 | import android.content.pm.ActivityInfo
24 | import androidx.compose.animation.*
25 | import androidx.compose.foundation.layout.Column
26 | import androidx.compose.foundation.layout.fillMaxSize
27 | import androidx.compose.foundation.layout.fillMaxWidth
28 | import androidx.compose.foundation.layout.padding
29 | import androidx.compose.material3.*
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.LaunchedEffect
32 | import androidx.compose.ui.Alignment
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType
35 | import androidx.compose.ui.input.nestedscroll.nestedScroll
36 | import androidx.compose.ui.platform.LocalHapticFeedback
37 | import androidx.compose.ui.res.stringResource
38 | import androidx.compose.ui.text.style.TextAlign
39 | import androidx.compose.ui.unit.dp
40 | import androidx.lifecycle.lifecycleScope
41 | import io.github.yamin8000.owl.R
42 | import io.github.yamin8000.owl.content.MainBottomBar
43 | import io.github.yamin8000.owl.content.MainTopBar
44 | import io.github.yamin8000.owl.ui.composable.*
45 | import kotlinx.coroutines.launch
46 | import java.util.*
47 |
48 | @OptIn(ExperimentalMaterial3Api::class)
49 | @Composable
50 | fun HomeContent(
51 | searchTerm: String?,
52 | onHistoryClick: () -> Unit,
53 | onFavouritesClick: () -> Unit,
54 | onInfoClick: () -> Unit,
55 | onSettingsClick: () -> Unit
56 | ) {
57 | LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
58 | Surface(
59 | modifier = Modifier.fillMaxSize(),
60 | content = {
61 | val state = rememberHomeState()
62 |
63 | if (state.listState.isScrollInProgress && state.isVibrating.value)
64 | LocalHapticFeedback.current.performHapticFeedback(HapticFeedbackType.TextHandleMove)
65 |
66 | InternetAwareComposable { state.isOnline.value = it }
67 |
68 | val locale = if (state.ttsLang.value.isEmpty())
69 | Locale.US else Locale.forLanguageTag(state.ttsLang.value)
70 |
71 | if (searchTerm != null) {
72 | state.searchText = searchTerm
73 | LaunchedEffect(Unit) { state.addSearchTextToHistory() }
74 | }
75 | LaunchedEffect(state.isOnline.value) {
76 | if (state.isFirstTimeOpening)
77 | state.searchText = "Owl"
78 | if (state.searchText.isNotBlank())
79 | state.searchForDefinition()
80 | }
81 |
82 | if (state.searchResult.value.item.isNotEmpty() && state.rawWordSearchBody.value != null && state.isSharing.value)
83 | state.handleShareIntent()
84 |
85 | val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
86 |
87 | Scaffold(
88 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
89 | snackbarHost = {
90 | SnackbarHost(state.snackbarHostState) { data ->
91 | MySnackbar {
92 | PersianText(
93 | text = data.visuals.message,
94 | modifier = Modifier.fillMaxWidth(),
95 | textAlign = TextAlign.Center
96 | )
97 | }
98 | }
99 | },
100 | topBar = {
101 | MainTopBar(
102 | scrollBehavior = scrollBehavior,
103 | onHistoryClick = onHistoryClick,
104 | onFavouritesClick = onFavouritesClick,
105 | onInfoClick = onInfoClick,
106 | onSettingsClick = onSettingsClick,
107 | onRandomWordClick = { state.searchForRandomWord() }
108 | )
109 | },
110 | bottomBar = {
111 | MainBottomBar(
112 | searchTerm = searchTerm,
113 | suggestions = state.searchSuggestions.value,
114 | isSearching = state.isSearching.value,
115 | onSearchTermChanged = {
116 | state.searchText = it
117 | state.scope.launch { state.handleSuggestions() }
118 | if (state.isWordSelectedFromKeyboardSuggestions) {
119 | state.scope.launch { state.searchForDefinitionHandler() }
120 | state.clearSuggestions()
121 | }
122 | },
123 | onSuggestionClick = {
124 | state.searchText = it
125 | state.lifecycleOwner.lifecycleScope.launch { state.searchForDefinitionHandler() }
126 | },
127 | onSearch = {
128 | state.searchText = it
129 | state.lifecycleOwner.lifecycleScope.launch { state.searchForDefinitionHandler() }
130 | },
131 | onCancel = { state.cancel() }
132 | )
133 | },
134 | content = { contentPadding ->
135 | Column(
136 | horizontalAlignment = Alignment.CenterHorizontally,
137 | modifier = Modifier
138 | .padding(contentPadding)
139 | .padding(top = 8.dp),
140 | content = {
141 | AnimatedVisibility(
142 | visible = !state.isOnline.value,
143 | enter = slideInVertically() + fadeIn(),
144 | exit = slideOutVertically() + fadeOut(),
145 | content = {
146 | PersianText(
147 | text = stringResource(R.string.general_net_error),
148 | modifier = Modifier.padding(16.dp),
149 | color = MaterialTheme.colorScheme.error
150 | )
151 | }
152 | )
153 |
154 | val addedToFavourites = stringResource(R.string.added_to_favourites)
155 |
156 | if (state.rawWordSearchBody.value != null || state.searchResult.value.item.isNotEmpty()) {
157 | state.rawWordSearchBody.value?.let { word ->
158 | WordCard(
159 | localeTag = locale.toLanguageTag(),
160 | word = word.word,
161 | pronunciation = word.pronunciation,
162 | onShareWord = { state.isSharing.value = true },
163 | onAddToFavourite = {
164 | state.scope.launch {
165 | state.addToFavourite(word.word)
166 | state.snackbarHostState.showSnackbar(
167 | addedToFavourites
168 | )
169 | }
170 | }
171 | )
172 | }
173 |
174 | WordDefinitionsList(
175 | word = state.rawWordSearchBody.value?.word ?: "",
176 | localeTag = locale.toLanguageTag(),
177 | listState = state.listState,
178 | searchResult = state.searchResult.value,
179 | onWordChipClick = {
180 | state.searchText = it
181 | state.lifecycleOwner.lifecycleScope.launch { state.searchForDefinitionHandler() }
182 | }
183 | )
184 | } else EmptyList()
185 | }
186 | )
187 | }
188 | )
189 | }
190 | )
191 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/github/yamin8000/owl/content/settings/Settings.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Owl: an android app for Owlbot Dictionary API
3 | * Settings.kt Created by Yamin Siahmargooei at 2022/9/20
4 | * This file is part of Owl.
5 | * Copyright (C) 2022 Yamin Siahmargooei
6 | *
7 | * Owl is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * Owl is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with Owl. If not, see .
19 | */
20 |
21 | package io.github.yamin8000.owl.content.settings
22 |
23 | import android.os.Build
24 | import android.speech.tts.TextToSpeech
25 | import androidx.compose.foundation.layout.*
26 | import androidx.compose.foundation.lazy.LazyColumn
27 | import androidx.compose.foundation.lazy.items
28 | import androidx.compose.foundation.selection.selectable
29 | import androidx.compose.foundation.selection.selectableGroup
30 | import androidx.compose.material.icons.Icons
31 | import androidx.compose.material.icons.twotone.DisplaySettings
32 | import androidx.compose.material.icons.twotone.Language
33 | import androidx.compose.material3.*
34 | import androidx.compose.runtime.*
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.platform.LocalLifecycleOwner
38 | import androidx.compose.ui.res.stringResource
39 | import androidx.compose.ui.semantics.Role
40 | import androidx.compose.ui.text.style.TextAlign
41 | import androidx.compose.ui.unit.dp
42 | import androidx.lifecycle.lifecycleScope
43 | import io.github.yamin8000.owl.R
44 | import io.github.yamin8000.owl.ui.composable.*
45 | import io.github.yamin8000.owl.ui.theme.DefaultCutShape
46 | import io.github.yamin8000.owl.util.speak
47 | import kotlinx.coroutines.launch
48 | import java.util.*
49 |
50 | @OptIn(ExperimentalMaterial3Api::class)
51 | @Composable
52 | fun SettingsContent(
53 | onThemeChanged: (ThemeSetting) -> Unit,
54 | onBackClick: () -> Unit
55 | ) {
56 | val state = rememberSettingsState()
57 | val scope = LocalLifecycleOwner.current.lifecycleScope
58 |
59 | var englishLanguages by remember { mutableStateOf(listOf()) }
60 | var textToSpeech: TextToSpeech? by remember { mutableStateOf(null) }
61 |
62 | TtsAwareFeature(
63 | ttsLanguageLocaleTag = state.ttsLang.value,
64 | onTtsReady = { tts ->
65 | textToSpeech = tts
66 | englishLanguages = tts.availableLanguages.filter {
67 | it.language == Locale.ENGLISH.language
68 | }
69 | }
70 | )
71 |
72 | ScaffoldWithTitle(
73 | title = stringResource(id = R.string.settings),
74 | onBackClick = onBackClick
75 | ) {
76 | Column(
77 | horizontalAlignment = Alignment.CenterHorizontally,
78 | verticalArrangement = Arrangement.spacedBy(8.dp)
79 | ) {
80 | GeneralSettings(
81 | isVibrating = state.isVibrating.value,
82 | isVibratingChange = { state.scope.launch { state.updateVibrationSetting(it) } }
83 | )
84 | ThemeSetting(state.themeSetting.value) { newTheme ->
85 | state.scope.launch { state.updateThemeSetting(newTheme) }
86 | onThemeChanged(newTheme)
87 | }
88 | TtsLanguageSetting(
89 | languages = englishLanguages,
90 | currentTtsTag = state.ttsLang.value,
91 | onTtsTagChanged = { tag ->
92 | scope.launch {
93 | state.updateTtsLang(tag)
94 | textToSpeech?.speak(Locale.forLanguageTag(tag).displayName)
95 | }
96 | }
97 | )
98 | }
99 | }
100 | }
101 |
102 | @Composable
103 | fun TtsLanguageSetting(
104 | currentTtsTag: String,
105 | languages: List,
106 | onTtsTagChanged: (String) -> Unit
107 | ) {
108 | var isDialogShown by remember { mutableStateOf(false) }
109 |
110 | if (isDialogShown) {
111 | TtsLanguagesDialog(
112 | currentTtsTag = currentTtsTag,
113 | languages = languages,
114 | onLanguageSelected = onTtsTagChanged,
115 | onDismiss = { isDialogShown = false }
116 | )
117 | }
118 |
119 | SettingsItemCard(
120 | title = stringResource(R.string.tts_language),
121 | content = {
122 | SettingsItem(
123 | onClick = { isDialogShown = true },
124 | content = {
125 | Icon(imageVector = Icons.TwoTone.Language, contentDescription = null)
126 | PersianText(Locale.forLanguageTag(currentTtsTag).displayName)
127 | }
128 | )
129 | }
130 | )
131 | }
132 |
133 | @Composable
134 | fun TtsLanguagesDialog(
135 | currentTtsTag: String,
136 | languages: List,
137 | onLanguageSelected: (String) -> Unit,
138 | onDismiss: () -> Unit
139 | ) {
140 | AlertDialog(
141 | onDismissRequest = { onDismiss() },
142 | title = { PersianText(stringResource(R.string.tts_language)) },
143 | icon = { Icon(imageVector = Icons.TwoTone.Language, contentDescription = null) },
144 | confirmButton = {/*ignored*/ },
145 | text = {
146 | LazyColumn(
147 | horizontalAlignment = Alignment.CenterHorizontally,
148 | verticalArrangement = Arrangement.spacedBy(8.dp),
149 | content = {
150 | items(languages) {
151 | TtsLanguageItem(
152 | localeTag = it.toLanguageTag(),
153 | isSelected = it.toLanguageTag() == currentTtsTag
154 | ) { tag ->
155 | onLanguageSelected(tag)
156 | onDismiss()
157 | }
158 | }
159 | }
160 | )
161 | }
162 | )
163 | }
164 |
165 | @Composable
166 | fun GeneralSettings(
167 | isVibrating: Boolean,
168 | isVibratingChange: (Boolean) -> Unit
169 | ) {
170 | SettingsItemCard(
171 | modifier = Modifier.fillMaxWidth(),
172 | title = stringResource(R.string.general),
173 | content = {
174 | Row(
175 | horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
176 | verticalAlignment = Alignment.CenterVertically
177 | ) {
178 | Icon(imageVector = Icons.TwoTone.Language, contentDescription = null)
179 | SwitchWithText(
180 | caption = stringResource(R.string.vibrate_on_scroll),
181 | checked = isVibrating,
182 | onCheckedChange = isVibratingChange
183 | )
184 | }
185 | }
186 | )
187 | }
188 |
189 | @OptIn(ExperimentalMaterial3Api::class)
190 | @Composable
191 | fun TtsLanguageItem(
192 | localeTag: String,
193 | isSelected: Boolean,
194 | onClick: ((String) -> Unit)? = null
195 | ) {
196 | OutlinedCard(
197 | modifier = Modifier.fillMaxWidth(),
198 | shape = DefaultCutShape,
199 | onClick = { onClick?.invoke(localeTag) },
200 | enabled = !isSelected
201 | ) {
202 | PersianText(
203 | text = Locale.forLanguageTag(localeTag).displayName,
204 | modifier = Modifier.padding(16.dp)
205 | )
206 | }
207 | }
208 |
209 | @Composable
210 | fun ThemeSetting(
211 | currentTheme: ThemeSetting,
212 | onCurrentThemeChange: (ThemeSetting) -> Unit
213 | ) {
214 | var isShowingThemeDialog by remember { mutableStateOf(false) }
215 |
216 | SettingsItemCard(
217 | title = stringResource(R.string.theme),
218 | content = {
219 | if (isShowingThemeDialog) {
220 | ThemeChangerDialog(
221 | currentTheme = currentTheme,
222 | onCurrentThemeChange = onCurrentThemeChange,
223 | onDismiss = { isShowingThemeDialog = false }
224 | )
225 | }
226 | SettingsItem(
227 | onClick = { isShowingThemeDialog = true },
228 | content = {
229 | Icon(
230 | imageVector = Icons.TwoTone.DisplaySettings,
231 | contentDescription = stringResource(R.string.theme)
232 | )
233 | PersianText(
234 | text = stringResource(currentTheme.persianNameStringResource),
235 | modifier = Modifier.padding()
236 | )
237 | }
238 | )
239 | if (currentTheme == ThemeSetting.System && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
240 | DynamicThemeNotice()
241 | }
242 | )
243 | }
244 |
245 | @Composable
246 | fun ThemeChangerDialog(
247 | currentTheme: ThemeSetting,
248 | onCurrentThemeChange: (ThemeSetting) -> Unit,
249 | onDismiss: () -> Unit
250 | ) {
251 | val themes = remember { ThemeSetting.values() }
252 | AlertDialog(
253 | onDismissRequest = onDismiss,
254 | confirmButton = { /*ignored*/ },
255 | title = { PersianText(stringResource(R.string.theme)) },
256 | icon = { Icon(imageVector = Icons.TwoTone.DisplaySettings, contentDescription = null) },
257 | text = {
258 | Column(
259 | horizontalAlignment = Alignment.CenterHorizontally,
260 | verticalArrangement = Arrangement.SpaceBetween,
261 | modifier = Modifier
262 | .padding(16.dp)
263 | .selectableGroup()
264 | .fillMaxWidth(),
265 | content = {
266 | themes.forEach { theme ->
267 | Row(
268 | verticalAlignment = Alignment.CenterVertically,
269 | horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.Start),
270 | modifier = Modifier
271 | .fillMaxWidth()
272 | .selectable(
273 | selected = (theme == currentTheme),
274 | role = Role.RadioButton,
275 | onClick = {
276 | onCurrentThemeChange(theme)
277 | onDismiss()
278 | }
279 | ),
280 | content = {
281 | RadioButton(
282 | selected = (theme == currentTheme),
283 | onClick = null,
284 | modifier = Modifier.padding(start = 8.dp)
285 | )
286 | PersianText(
287 | text = stringResource(theme.persianNameStringResource),
288 | modifier = Modifier.padding(vertical = 16.dp)
289 | )
290 | }
291 | )
292 | }
293 | }
294 | )
295 | }
296 | )
297 | }
298 |
299 | @Composable
300 | fun DynamicThemeNotice() {
301 | PersianText(
302 | text = stringResource(R.string.dynamic_theme_notice),
303 | textAlign = TextAlign.Justify
304 | )
305 | }
--------------------------------------------------------------------------------