Merge remote-tracking branch 'origin/master' into desirepath

# Conflicts:
#	app/src/main/java/io/legado/app/base/BaseActivity.kt
pull/32/head
Invinciblelee 6 years ago
commit 8b1a462aff
  1. 7
      app/build.gradle
  2. 4
      app/src/main/AndroidManifest.xml
  3. 16
      app/src/main/assets/disclaimer.md
  4. BIN
      app/src/main/assets/number.ttf
  5. 38
      app/src/main/assets/txtChapterRule.json
  6. 5
      app/src/main/assets/updateLog.md
  7. 176
      app/src/main/assets/web/bookshelf.css
  8. 32
      app/src/main/assets/web/bookshelf.html
  9. 161
      app/src/main/assets/web/bookshelf.js
  10. BIN
      app/src/main/assets/web/favicon.ico
  11. 148
      app/src/main/assets/web/index.css
  12. 306
      app/src/main/assets/web/index.html
  13. 362
      app/src/main/assets/web/index.js
  14. 99
      app/src/main/java/io/legado/app/App.kt
  15. 73
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  16. 117
      app/src/main/java/io/legado/app/lib/theme/ATH.java
  17. 116
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  18. 31
      app/src/main/java/io/legado/app/lib/theme/ATHUtil.java
  19. 24
      app/src/main/java/io/legado/app/lib/theme/ATHUtil.kt
  20. 80
      app/src/main/java/io/legado/app/lib/theme/ColorUtil.java
  21. 79
      app/src/main/java/io/legado/app/lib/theme/ColorUtil.kt
  22. 28
      app/src/main/java/io/legado/app/lib/theme/DrawableUtil.java
  23. 25
      app/src/main/java/io/legado/app/lib/theme/DrawableUtil.kt
  24. 52
      app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.java
  25. 45
      app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt
  26. 55
      app/src/main/java/io/legado/app/lib/theme/NavigationViewUtil.java
  27. 38
      app/src/main/java/io/legado/app/lib/theme/NavigationViewUtil.kt
  28. 430
      app/src/main/java/io/legado/app/lib/theme/Selector.java
  29. 443
      app/src/main/java/io/legado/app/lib/theme/Selector.kt
  30. 318
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.java
  31. 309
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt
  32. 92
      app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.java
  33. 92
      app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt
  34. 29
      app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.java
  35. 31
      app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt
  36. 384
      app/src/main/java/io/legado/app/lib/theme/TintHelper.java
  37. 455
      app/src/main/java/io/legado/app/lib/theme/TintHelper.kt
  38. 48
      app/src/main/java/io/legado/app/lib/theme/ViewUtil.java
  39. 41
      app/src/main/java/io/legado/app/lib/theme/ViewUtil.kt
  40. 36
      app/src/main/java/io/legado/app/lib/theme/prefs/ATEPreferenceCategory.kt
  41. 52
      app/src/main/java/io/legado/app/lib/theme/prefs/ATESwitchPreference.kt
  42. 95
      app/src/main/java/io/legado/app/lib/theme/prefs/IconListPreference.kt
  43. 33
      app/src/main/java/io/legado/app/lib/theme/view/ATEAccentBgTextView.kt
  44. 39
      app/src/main/java/io/legado/app/lib/theme/view/ATEAccentStrokeTextView.kt
  45. 33
      app/src/main/java/io/legado/app/lib/theme/view/ATEAutoCompleteTextView.kt
  46. 29
      app/src/main/java/io/legado/app/lib/theme/view/ATECheckBox.kt
  47. 29
      app/src/main/java/io/legado/app/lib/theme/view/ATEEditText.kt
  48. 28
      app/src/main/java/io/legado/app/lib/theme/view/ATEPrimaryTextView.kt
  49. 41
      app/src/main/java/io/legado/app/lib/theme/view/ATEProgressBar.kt
  50. 29
      app/src/main/java/io/legado/app/lib/theme/view/ATERadioButton.kt
  51. 37
      app/src/main/java/io/legado/app/lib/theme/view/ATERadioNoButton.kt
  52. 28
      app/src/main/java/io/legado/app/lib/theme/view/ATESecondaryTextView.kt
  53. 29
      app/src/main/java/io/legado/app/lib/theme/view/ATESeekBar.kt
  54. 34
      app/src/main/java/io/legado/app/lib/theme/view/ATEStockSwitch.kt
  55. 41
      app/src/main/java/io/legado/app/lib/theme/view/ATEStrokeTextView.kt
  56. 34
      app/src/main/java/io/legado/app/lib/theme/view/ATESwitch.kt
  57. 31
      app/src/main/java/io/legado/app/lib/theme/view/ATETextInputLayout.kt
  58. 16
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  59. 2
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  60. 21
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  61. 11
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.java
  62. 3
      app/src/main/java/io/legado/app/ui/search/SearchActivity.kt
  63. 7
      app/src/main/java/io/legado/app/utils/AlertDialogExtensions.kt
  64. 16
      app/src/main/java/io/legado/app/utils/FloatExtensions.kt
  65. 13
      app/src/main/java/io/legado/app/utils/IntExtensions.kt
  66. 5
      app/src/main/res/drawable-v21/bg_ib_pre.xml
  67. 3
      app/src/main/res/drawable-v21/bg_ib_pre_round.xml
  68. 10
      app/src/main/res/drawable/bg_chapter_item_divider.xml
  69. 6
      app/src/main/res/drawable/bg_edit.xml
  70. 5
      app/src/main/res/drawable/bg_ib_pre.xml
  71. BIN
      app/src/main/res/drawable/bg_shadow_bottom.png
  72. BIN
      app/src/main/res/drawable/bg_shadow_bottom_night.png
  73. BIN
      app/src/main/res/drawable/bg_shadow_top.png
  74. BIN
      app/src/main/res/drawable/bg_shadow_top_night.png
  75. 10
      app/src/main/res/drawable/bg_textfield_search.xml
  76. 39
      app/src/main/res/drawable/fastscroll_bubble.xml
  77. 31
      app/src/main/res/drawable/fastscroll_handle.xml
  78. 27
      app/src/main/res/drawable/fastscroll_track.xml
  79. 11
      app/src/main/res/drawable/ic_about.xml
  80. 11
      app/src/main/res/drawable/ic_add.xml
  81. 11
      app/src/main/res/drawable/ic_add_online.xml
  82. 17
      app/src/main/res/drawable/ic_arrange.xml
  83. 9
      app/src/main/res/drawable/ic_arrow_back.xml
  84. 9
      app/src/main/res/drawable/ic_arrow_drop_down.xml
  85. 9
      app/src/main/res/drawable/ic_arrow_drop_up.xml
  86. 12
      app/src/main/res/drawable/ic_author.xml
  87. 14
      app/src/main/res/drawable/ic_auto_page.xml
  88. 17
      app/src/main/res/drawable/ic_auto_page_stop.xml
  89. 11
      app/src/main/res/drawable/ic_back_last.xml
  90. 29
      app/src/main/res/drawable/ic_backup.xml
  91. 14
      app/src/main/res/drawable/ic_book_has.xml
  92. 15
      app/src/main/res/drawable/ic_book_last.xml
  93. 20
      app/src/main/res/drawable/ic_book_source_manage.xml
  94. 11
      app/src/main/res/drawable/ic_bookmark.xml
  95. 11
      app/src/main/res/drawable/ic_brightness.xml
  96. 14
      app/src/main/res/drawable/ic_cancel.xml
  97. 20
      app/src/main/res/drawable/ic_chapter_list.xml
  98. 9
      app/src/main/res/drawable/ic_check.xml
  99. 14
      app/src/main/res/drawable/ic_check_source.xml
  100. 17
      app/src/main/res/drawable/ic_copy.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -9,9 +9,6 @@ androidExtensions {
android { android {
compileSdkVersion 28 compileSdkVersion 28
dataBinding {
enabled = true
}
defaultConfig { defaultConfig {
applicationId "io.legado.app" applicationId "io.legado.app"
minSdkVersion 21 minSdkVersion 21
@ -85,6 +82,6 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.8.0' implementation 'com.github.bumptech.glide:glide:4.8.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
} }

@ -23,7 +23,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/CAppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning"> tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity android:name=".ui.search.SearchActivity"> <activity android:name=".ui.search.SearchActivity">
</activity> </activity>
@ -32,7 +32,7 @@
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"> android:theme="@style/CAppTheme.NoActionBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>

@ -0,0 +1,16 @@
# 免责声明(Disclaimer)
* 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。
* 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。
各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。
任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。
第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。
您应该对使用搜索引擎的结果自行承担风险。
* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。
因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。
阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。
* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。
阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。
阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。
任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。
阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。

Binary file not shown.

@ -0,0 +1,38 @@
[
{
"enable": true,
"name": "默认正则1",
"rule": "^(.{0,8})(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节卷集部篇回场])(.{0,30})$",
"serialNumber": 0
},
{
"enable": true,
"name": "默认正则2",
"rule": "^([0-9]{1,5})([\\,\\.,-])(.{1,20})$",
"serialNumber": 1
},
{
"enable": true,
"name": "默认正则3",
"rule": "^(\\s{0,4})([\\(【《]?(卷)?)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([\\.:: \f\t])(.{0,30})$",
"serialNumber": 2
},
{
"enable": true,
"name": "默认正则4",
"rule": "^(\\s{0,4})([\\((【《])(.{0,30})([\\))】》])(\\s{0,2})$",
"serialNumber": 3
},
{
"enable": true,
"name": "默认正则5",
"rule": "^(\\s{0,4})(正文)(.{0,20})$",
"serialNumber": 4
},
{
"enable": true,
"name": "默认正则6",
"rule": "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$",
"serialNumber": 5
}
]

@ -0,0 +1,5 @@
## 本软件为开源软件,没有上架Google Play,请不要在任何地方购买!
### 关注公众号[开源阅读软件]可获取书源,点击广告可支持作者!
### 捐赠里点击红包搜索码可开启高级功能!
## 更新日志

@ -0,0 +1,176 @@
html, body {
height: 100%;
margin: 0;
}
.hide {
display: none;
}
.top, .showchapter, .hidebooks {
width: 60px;
height: 50px;
position: absolute;
right: 30px;
bottom: 30px;
color: black;
font-size: 28px;
background-color: #ddd;
opacity: 0.85;
}
.top {
bottom: 150px;
}
.showchapter {
bottom: 90px;
bottom: 90px;
}
.address {
width: 270px;
}
.nav {
border-bottom: solid 1px #ccc;
}
input, button {
width: 110px;
line-height: 34px;
background-color: #eee;
color: #555;
border: none;
margin: 10px 5px;
font-weight: 500;
border-radius: 2px;
outline: none;
cursor: pointer;
}
input {
padding: 0 10px;
cursor: text;
}
input:hover, button:hover {
border-color: #aaa;
background-color: #efefef;
color: #222;
outline: solid 1px #ccc;
}
.allcontent {
height: calc(100% - 60px);
}
.allscreen {
height: 100%
}
.books > div {
display: inline-block;
margin: 10px;
vertical-align: top;
border: solid 1px #ddd;
}
.read > .books {
width: 420px;
float: left;
height: 100%;
overflow: auto;
border-right: solid 1px #ccc;
}
.read > .books > div {
margin-right: 0;
border-right: none;
}
.more {
overflow-y: auto;
height: 100%;
display: none;
}
.read .more {
display: block;
}
.books > div > img {
width: 120px;
height: 180px;
float: left;
margin-right: 10px;
cursor: pointer;
}
.info {
padding: 10px 20px 0 20px;
width: 600px;
margin: 0 auto;
}
.info > img {
width: 600px;
height: 900px;
}
.info p {
line-height: 1.5;
text-align: justify;
margin: 0;
}
.books tr:nth-child(n+2) td {
border-top: solid 1px #999;
}
.books td:nth-child(1) {
vertical-align: top;
width: 50px;
}
.books td:nth-child(2) {
vertical-align: top;
width: 200px;
}
.clear {
clear: both;
}
.chapter {
margin: 10px;
max-height: 500px;
overflow-y: auto;
border-top: solid 1px #333;
border-bottom: solid 1px #333;
}
.chapter button {
width: 230px;
text-align: left;
text-indent: 14px;
margin: 10px 4px;
}
.content {
padding: 20px;
text-align: justify;
min-height: 1000px;
padding-bottom: 200px;
}
.content h2 {
font-family: "Microsoft YaHei",微软雅黑,"MicrosoftJhengHei",华文细黑,STHeiti,MingLiu;
font-weight: 500;
text-align: center;
line-height: 100px;
font-size: 40px;
margin: 0;
}

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>阅读书架</title>
<link href="bookshelf.css" rel="stylesheet"/>
</head>
<body>
<button id="top" class="top"></button>
<button id="showchapter" class="showchapter"></button>
<button id="hidebooks" class="hidebooks"></button>
<div class="nav">
<button id="back">返回</button>
<button id="type">所有书籍 ▼</button>
<button id="sort">手动排序 ▼</button>
<button id="setting">阅读设置</button>
<input type="text" class="address" id="address" title="阅读APP地址或IP" value=""/>
<button id="refresh">重新加载</button>
</div>
<div class="allcontent" id="allcontent">
<div id="books" class="books"></div>
<div id="more" class="more">
<div id="info" class="info"></div>
<div class="clear"></div>
<div id="chapter" class="chapter"></div>
<div id="content" class="content"></div>
</div>
</div>
<script src="bookshelf.js"></script>
</body>
</html>

@ -0,0 +1,161 @@
var $ = document.querySelector.bind(document)
, $$ = document.querySelectorAll.bind(document)
, $c = document.createElement.bind(document)
, randomImg = "http://acg.bakayun.cn/randbg.php?t=dfzh"
, randomImg2 = "http://img.xjh.me/random_img.php"
, books
;
var formatTime = value => {
return new Date(value).toLocaleString('zh-CN', {
hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"
}).replace(/\//g, "-");
};
var apiMap = {
getBookshelf: "/getBookshelf",
getChapterList: "/getChapterList",
getBookContent: "/getBookContent",
saveBook: "/saveBook"
};
var apiAddress = (apiName, url) => {
let address = $('#address').value || window.location.host;
if (!(/^http|^\/\//).test(address)) {
address = "//" + address;
}
if (!(/:\d{4,}/).test(address.split("//")[1].split("/")[0])) {
address += ":1122";
}
localStorage.setItem('address', address);
return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "");
};
var init = () => {
$('#allcontent').classList.remove("read");
$('#books').innerHTML = "";
fetch(apiAddress("getBookshelf"), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(getBookshelf.errorMsg);
return;
}
books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber);
books.forEach(book => {
let bookDiv = $c("div");
let img = $c("img");
img.src = book.bookInfoBean.coverUrl || randomImg;
img.setAttribute("data-series-num", book.serialNumber);
bookDiv.appendChild(img);
bookDiv.innerHTML += `<table><tbody>
<tr><td>书名</td><td>${book.bookInfoBean.name}</td></tr>
<tr><td>作者</td><td>${book.bookInfoBean.author}</td></tr>
<tr><td>阅读</td><td>${book.durChapterName}<br>${formatTime(book.finalDate)}</td></tr>
<tr><td>更新</td><td>${book.lastChapterName}<br>${formatTime(book.finalRefreshData)}</td></tr>
<tr><td>来源</td><td>${book.bookInfoBean.origin}</td></tr>
</tbody></table>`;
$('#books').appendChild(bookDiv);
});
$$('#books img').forEach(bookImg =>
bookImg.addEventListener("click", () => {
$('#allcontent').classList.add("read");
var book = books[bookImg.getAttribute("data-series-num")];
$("#info").innerHTML = `<img src="${bookImg.src}">
<p>  来源${book.bookInfoBean.origin}</p>
<p>  书名${book.bookInfoBean.name}</p>
<p>  作者${book.bookInfoBean.author}</p>
<p>阅读章节${book.durChapterName}</p>
<p>阅读时间${formatTime(book.finalDate)}</p>
<p>最新章节${book.lastChapterName}</p>
<p>检查时间${formatTime(book.finalRefreshData)}</p>
<p>  简介${book.bookInfoBean.introduce.trim().replace(/\n/g, "<br>")}</p>`;
window.location.hash = "";
window.location.hash = "#info";
$("#content").innerHTML = "章节列表加载中...";
$("#chapter").innerHTML = "";
fetch(apiAddress("getChapterList", book.noteUrl), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(data.errorMsg);
$("#content").innerHTML = "章节列表加载失败!";
return;
}
data.data.forEach(chapter => {
let ch = $c("button");
ch.setAttribute("data-url", chapter.durChapterUrl);
ch.setAttribute("title", chapter.durChapterName);
ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName;
$("#chapter").appendChild(ch);
});
$('#chapter').scrollTop = 0;
$("#content").innerHTML = "章节列表加载完成!";
});
}));
});
};
$("#back").addEventListener("click", () => {
if (window.location.hash === "#content") {
window.location.hash = "#chapter";
} else if (window.location.hash === "#chapter") {
window.location.hash = "#info";
} else {
$('#allcontent').classList.remove("read");
}
});
$("#refresh").addEventListener("click", init);
$('#hidebooks').addEventListener("click", () => {
$("#books").classList.toggle("hide");
$(".nav").classList.toggle("hide");
$("#allcontent").classList.toggle("allscreen");
});
$('#top').addEventListener("click", () => {
window.location.hash = "";
window.location.hash = "#info";
});
$('#showchapter').addEventListener("click", () => {
window.location.hash = "";
window.location.hash = "#chapter";
});
$('#chapter').addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
var url = e.target.getAttribute("data-url");
var name = e.target.getAttribute("title");
if (!url) {
alert("未取得章节地址");
}
$("#content").innerHTML = "<p>" + name + " 加载中...</p>";
fetch(apiAddress("getBookContent", url), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(data.errorMsg);
$("#content").innerHTML = "<p>" + name + " 加载失败!</p>";
return;
}
var content = data.data.trim().split("\n\n");
if (content.length === 2) {
$("#content").innerHTML = `<h2>${content[0]}</h2>  (全文 ${content[1].length} 字)<br><br>  ` + content[1].trim().replace(/\n/g, "<br><br>");
} else {
$("#content").innerHTML = `<h2>${name || e.target.innerHTML}</h2>  (全文 ${data.data.length} 字)<br><br>  ` + data.data.trim().replace(/\n/g, "<br><br>");
}
window.location.hash = "";
window.location.hash = "#content";
});
}
});
$('#address').setAttribute("placeholder", "阅读APP地址或IP:" + window.location.host);
if (!$('#address').value && typeof localStorage && localStorage.getItem('address')) {
$('#address').value = localStorage.getItem('address');
}
init();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,148 @@
body {
margin: 0;
}
.editor {
display: flex;
align-items: stretch;
}
.setbox,
.menu,
.outbox {
flex: 1;
display: flex;
flex-flow: column;
max-height: 100vh;
overflow-y: auto;
}
.menu {
justify-content: center;
max-width: 90px;
margin: 0 5px;
}
.menu .button {
width: 90px;
height: 30px;
min-height: 30px;
margin: 5px 0px;
cursor: pointer;
}
@keyframes stroker {
0% {
stroke-dashoffset: 0
}
100% {
stroke-dashoffset: -240
}
}
.button rect {
width: 100%;
height: 100%;
fill: transparent;
stroke: #666;
stroke-width: 2px;
}
.button rect.busy {
stroke: #fD1850;
stroke-dasharray: 30 90;
animation: stroker 1s linear infinite;
}
.button text {
text-anchor: middle;
dominant-baseline: middle;
}
.setbox {
min-width: 40em;
}
.rules,
.tabbox {
flex: 1;
display: flex;
flex-flow: column;
}
.rules>* {
display: flex;
margin: 2px 0;
}
.rules textarea {
flex: 1;
margin-left: 5px;
}
.rules>*,
.rules>*>div,
.rules textarea {
min-height: 1em;
}
textarea {
word-break: break-all;
}
.tabtitle {
display: flex;
z-index: 1;
justify-content: flex-end;
}
.tabtitle>div {
cursor: pointer;
padding: 1px 10px 0 10px;
border-bottom: 3px solid transparent;
font-weight: bold;
}
.tabtitle>.this {
color: #4f9da6;
border-bottom-color: #4EBBE4;
}
.tabbody {
flex: 1;
display: flex;
margin-top: -1px;
border: 1px solid #A9A9A9;
height: 0;
}
.tabbody>* {
flex: 1;
flex-flow: column;
display: none;
}
.tabbody>.this {
display: flex;
}
.tabbody>*>.titlebar{
display: flex;
}
.tabbody>*>.titlebar>*{
flex: 1;
margin: 1px 1px 1px 1px;
}
.tabbody>*>.context {
flex: 1;
flex-flow: column;
border: 0;
padding: 5px;
overflow-y: auto;
}
.tabbody>*>.inputbox{
border: 0;
border-bottom: #A9A9A9 solid 1px;
height: 15px;
text-align:center;
}
.link>* {
display: flex;
margin: 5px;
border-bottom: 1px solid;
text-decoration: none;
}
#RuleList>label>* {
background: #eee;
padding-left: 3px;
margin: 2px 0;
cursor: pointer;
}
#RuleList input[type=radio] {
display: none;
}
#RuleList input[type="radio"]:checked+* {
background: #15cda8;
}
.isError {
color: #FF0000;
}

@ -0,0 +1,306 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>书源编辑器v3.8</title>
<link rel="stylesheet" type="text/css" href="/index.css"/>
</head>
<body>
<div class="editor">
<div class="setbox">
<div class="rules">
<div><b>书源基础信息</b></div>
<div>
<div>书源名称:</div>
<textarea rows="1" id="bookSourceName" placeholder="书源名称(bookSourceName) | 会显示在书源列表"></textarea>
</div>
<div>
<div>书源分组:</div>
<textarea rows="1" id="bookSourceGroup" placeholder="书源分组(bookSourceGroup) | 描述书源的特征信息"></textarea>
</div>
<div>
<div>书源域名:</div>
<textarea rows="1" id="bookSourceUrl"
placeholder="书源URL(bookSourceUrl) | 通常填写网站主页(标头不可省略),例: https://www.qidian.com"></textarea>
</div>
<div>
<div>登录网页:</div>
<textarea rows="1" id="loginUrl" placeholder="登录URL(loginUrl) | 填写网站登录网址,仅在需要登录的书源有用"></textarea>
</div>
<div><b>书籍发现规则</b></div>
<div>
<div>发现菜单:</div>
<textarea rows="5" id="ruleFindUrl"
placeholder="发现分类菜单规则(ruleFindUrl),将显示在发现菜单&#10;每行一条发现分类(网址域名可省略):&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
</div>
<div>
<div>结果列表:</div>
<textarea rows="1" id="ruleFindList"
placeholder="发现页列表规则(ruleFindList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>书籍名称:</div>
<textarea rows="1" id="ruleFindName"
placeholder="发现页书名规则(ruleFindName) | 选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍作者:</div>
<textarea rows="1" id="ruleFindAuthor"
placeholder="发现页作者规则(ruleFindAuthor) | 选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍分类:</div>
<textarea rows="1" id="ruleFindKind"
placeholder="发现页分类规则(ruleFindKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleFindLastChapter"
placeholder="发现页最新章节规则(ruleFindLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介内容:</div>
<textarea rows="1" id="ruleFindIntroduce"
placeholder="发现页简介规则(ruleFindIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面链接:</div>
<textarea rows="1" id="ruleFindCoverUrl"
placeholder="发现页封面规则(ruleFindCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea>
</div>
<div>
<div>详情链接:</div>
<textarea rows="1" id="ruleFindNoteUrl"
placeholder="发现页详情规则(ruleFindNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea>
</div>
<div><b>书籍搜索规则</b></div>
<div>
<div>搜索网址:</div>
<textarea rows="1" id="ruleSearchUrl"
placeholder="搜索网址(ruleSearchUrl) | [域名可省略]/search.php@kw=searchKey|char=utf-8"></textarea>
</div>
<div>
<div>结果验证:</div>
<textarea rows="1" id="ruleBookUrlPattern"
placeholder="搜索页URL验证(ruleBookUrlPattern) | 正则验证URL是否为详情页,成功则跳过搜索页解析"></textarea>
</div>
<div>
<div>结果列表:</div>
<textarea rows="1" id="ruleSearchList"
placeholder="搜索页列表规则(ruleSearchList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>书籍名称:</div>
<textarea rows="1" id="ruleSearchName"
placeholder="搜索页书名规则(ruleSearchName) | 选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍作者:</div>
<textarea rows="1" id="ruleSearchAuthor"
placeholder="搜索页作者规则(ruleSearchAuthor) | 选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍分类:</div>
<textarea rows="1" id="ruleSearchKind"
placeholder="搜索页分类规则(ruleSearchKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleSearchLastChapter"
placeholder="搜索页最新章节规则(ruleSearchLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介内容:</div>
<textarea rows="1" id="ruleSearchIntroduce"
placeholder="搜索页简介规则(ruleSearchIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面链接:</div>
<textarea rows="1" id="ruleSearchCoverUrl"
placeholder="搜索页封面规则(ruleSearchCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea>
</div>
<div>
<div>详情链接:</div>
<textarea rows="1" id="ruleSearchNoteUrl"
placeholder="搜索页详情规则(ruleSearchNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea>
</div>
<div><b>书籍详情规则</b></div>
<div>
<div>页面处理:</div>
<textarea rows="1" id="ruleBookInfoInit"
placeholder="详情页信息预处理(ruleBookInfoInit) | 用于加速详情信息检索"></textarea>
</div>
<div>
<div>书籍名称:</div>
<textarea rows="1" id="ruleBookName"
placeholder="书名规则(ruleBookName) | 选择详情页书名 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍作者:</div>
<textarea rows="1" id="ruleBookAuthor"
placeholder="作者规则(ruleBookAuthor) | 选择详情页作者 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍分类:</div>
<textarea rows="1" id="ruleBookKind"
placeholder="分类规则(ruleBookKind) | 选择详情页分类信息 (规则结果为List&lt;String&gt;)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleBookLastChapter"
placeholder="最新章节规则(ruleBookLastChapter) | 选择详情页最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介内容:</div>
<textarea rows="1" id="ruleIntroduce"
placeholder="简介规则(ruleIntroduce) | 选择详情页书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面链接:</div>
<textarea rows="1" id="ruleCoverUrl" placeholder="封面规则(ruleCoverUrl) | 选择详情页书籍封面 (规则结果为Url)"></textarea>
</div>
<div>
<div>目录链接:</div>
<textarea rows="1" id="ruleChapterUrl"
placeholder="目录URL规则(ruleChapterUrl) | 选择目录页网址 (规则结果为Url, 与详情页相同时可省略)"></textarea>
</div>
<div><b>目录列表规则</b></div>
<div>
<div>目录翻页:</div>
<textarea rows="1" id="ruleChapterUrlNext"
placeholder="目录下一页规则(ruleChapterUrlNext) | 选择目录下一页链接 (规则结果为List&lt;Url&gt;)"></textarea>
</div>
<div>
<div>目录列表:</div>
<textarea rows="1" id="ruleChapterList"
placeholder="目录列表规则(ruleChapterList) | 选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>章节名称:</div>
<textarea rows="1" id="ruleChapterName"
placeholder="章节名称规则(ruleChapterName) | 选择章节名称 (规则结果为String)"></textarea>
</div>
<div>
<div>章节链接:</div>
<textarea rows="1" id="ruleContentUrl"
placeholder="章节URL规则(ruleContentUrl) | 选择章节链接 (规则结果为Url)"></textarea>
</div>
<div><b>正文阅读规则</b></div>
<div>
<div>章节正文:</div>
<textarea rows="1" id="ruleBookContent"
placeholder="正文规则(ruleBookContent) | 选择正文内容 (规则结果为String)"></textarea>
</div>
<div>
<div>正文翻页:</div>
<textarea rows="1" id="ruleContentUrlNext"
placeholder="正文翻页URL规则(ruleContentUrlNext) | 选择下一分页(不是下一章)链接 (规则结果为Url)"></textarea>
</div>
<div><b>其它规则</b></div>
<div>
<div>浏览标识:</div>
<textarea rows="1" id="httpUserAgent"
placeholder="浏览器UA(HttpUserAgent) | 浏览器标识:User-Agent (可选)"></textarea>
</div>
<div>
<div>排序编号:</div>
<textarea rows="1" id="serialNumber" placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea>
</div>
<div>
<div>搜索权重:</div>
<textarea rows="1" id="weight" placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea>
</div>
<div>
<div>是否启用:</div>
<textarea rows="1" id="enable" placeholder="默认启用=true,手动启用=false (可选,默认true)"></textarea>
</div>
</div>
</div>
<div class="menu">
<svg class="button">
<text x="50%" y="55%">⇈推送书源</text>
<rect id="push"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⇊拉取书源</text>
<rect id="pull"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⋘编辑书源</text>
<rect id="editor"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⋙生成书源</text>
<rect id="conver"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">✗清空表单</text>
<rect id="initial"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">↶撤销操作</text>
<rect id="undo"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">↷重做操作</text>
<rect id="redo"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⇏调试书源</text>
<rect id="debug"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">✓保存书源</text>
<rect id="accept"></rect>
</svg>
</div>
<div class="outbox">
<div class="tabbox">
<div class="tabtitle">
<div name="编辑书源" class="tab1 this">编辑书源</div>
<div name="调试书源" class="tab2">调试书源</div>
<div name="书源列表" class="tab3">书源列表</div>
<div name="帮助信息" class="tab4">帮助信息</div>
</div>
<div class="tabbody">
<div class="tab1 this">
<textarea class="context" id="RuleJsonString" placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea>
</div>
<div class="tab2">
<input type="text" class="inputbox" id="DebugKey" placeholder="我的">
<textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea>
</div>
<div class="tab3">
<div class="titlebar">
<button id="Import">导入书源文件</button>
<button id="Export">导出书源文件</button>
<button id="Delete">删除选中书源</button>
<button id="ClrAll">清空当前列表</button>
</div>
<div class="context" id="RuleList"></div>
</div>
<div class="tab4">
<div class="context link">
<a target="_blank" href="https://gedoor.github.io/MyBookshelf/sourcerule.html">官方书源教程</a>
<a target="_blank" href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</a>
<a target="_blank" href="https://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a>
<a target="_blank" href="https://www.w3cschool.cn/regex_rmjc/?">正则表达式教程</a>
<a target="_blank" href="https://regexr.com/">正则表达式在线验证工具</a>
<div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义
<br>(?s) 前缀表示跨行解析
<br>(?m) 前缀表示逐行匹配
<br>(?i) 前缀表示忽略大小写
</div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="/index.js"></script>
</body>
</html>

@ -0,0 +1,362 @@
// 简化js原生选择器
function $(selector) { return document.querySelector(selector); }
function $$(selector) { return document.querySelectorAll(selector); }
// 读写Hash值(val未赋值时为读取)
function hashParam(key, val) {
let hashstr = decodeURIComponent(window.location.hash);
let regKey = new RegExp(`${key}=([^&]*)`);
let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;
if (val == undefined) return getVal;
if (hashstr == '' || hashstr == '#') {
window.location.hash = `#${key}=${val}`;
}
else {
if (getVal) window.location.hash = hashstr.replace(getVal, val);
else {
window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`;
}
}
}
// 创建书源规则容器对象
const RuleJSON = (() => {
let ruleJson = {};
$$('.rules textarea').forEach(item => ruleJson[item.id] = '');
// for (let item of $$('.rules textarea')) ruleJson[item.id] = '';
ruleJson.serialNumber = 0;
ruleJson.weight = 0;
ruleJson.enable = true;
return ruleJson;
})();
// 选项卡Tab切换事件处理
function showTab(tabName) {
$$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });
$$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); });
$(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this';
$(`.tabtitle>*[name=${tabName}]`).className += ' this';
hashParam('tab', tabName);
}
// 书源列表列表标签构造函数
function newRule(rule) {
return `<label for="${rule.bookSourceUrl}"><input type="radio" name="rule" id="${rule.bookSourceUrl}"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`;
}
// 缓存规则列表
var RuleSources = [];
if (localStorage.getItem('RuleSources')) {
RuleSources = JSON.parse(localStorage.getItem('RuleSources'));
RuleSources.forEach(item => $('#RuleList').innerHTML += newRule(item));
}
// 页面加载完成事件
window.onload = () => {
$$('.tabtitle>*').forEach(item => {
item.addEventListener('click', () => {
showTab(item.innerHTML);
});
});
if (hashParam('tab')) showTab(hashParam('tab'));
}
// 获取数据
function HttpGet(url) {
return fetch(hashParam('domain') ? hashParam('domain') + url : url)
.then(res => res.json()).catch(err => console.error('Error:', err));
}
// 提交数据
function HttpPost(url, data) {
return fetch(hashParam('domain') ? hashParam('domain') + url : url, {
body: JSON.stringify(data),
method: 'POST',
mode: "cors",
headers: new Headers({
'Content-Type': 'application/json;charset=utf-8'
})
}).then(res => res.json()).catch(err => console.error('Error:', err));
}
// 将书源表单转化为书源对象
function rule2json() {
Object.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value);
RuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber);
RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
RuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
return RuleJSON;
}
// 将书源对象填充到书源表单
function json2rule(RuleEditor) {
Object.keys(RuleJSON).forEach((key) => $("#" + key).value = RuleEditor[key] ? RuleEditor[key] : '');
}
// 记录操作过程
var course = { "old": [], "now": {}, "new": [] };
if (localStorage.getItem('course')) {
course = JSON.parse(localStorage.getItem('course'));
json2rule(course.now);
}
else {
course.now = rule2json();
window.localStorage.setItem('course', JSON.stringify(course));
}
function todo() {
course.old.push(Object.assign({}, course.now));
course.now = rule2json();
course.new = [];
if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小
localStorage.setItem('course', JSON.stringify(course));
}
function undo() {
course = JSON.parse(localStorage.getItem('course'));
if (course.old.length > 0) {
course.new.push(course.now);
course.now = course.old.pop();
localStorage.setItem('course', JSON.stringify(course));
json2rule(course.now);
}
}
function redo() {
course = JSON.parse(localStorage.getItem('course'));
if (course.new.length > 0) {
course.old.push(course.now);
course.now = course.new.pop();
localStorage.setItem('course', JSON.stringify(course));
json2rule(course.now);
}
}
function setRule(editRule) {
let checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl);
if ($(`input[id="${editRule.bookSourceUrl}"]`)) {
Object.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; });
$(`input[id="${editRule.bookSourceUrl}"]+*`).innerHTML = `${editRule.bookSourceName}<br>${editRule.bookSourceUrl}`;
} else {
RuleSources.push(editRule);
$('#RuleList').innerHTML += newRule(editRule);
}
}
$$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) });
$$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) });
// 处理按钮点击事件
$('.menu').addEventListener('click', e => {
let thisNode = e.target;
thisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') :
thisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null;
if (!thisNode) return;
if (thisNode.getAttribute('class') == 'busy') return;
thisNode.setAttribute('class', 'busy');
switch (thisNode.id) {
case 'push':
$$('#RuleList>label>div').forEach(item => { item.className = ''; });
(async () => {
await HttpPost(`/saveSources`, RuleSources).then(json => {
if (json.isSuccess) {
let okData = json.data;
if (Array.isArray(okData)) {
let failMsg = ``;
if (RuleSources.length > okData.length) {
RuleSources.forEach(item => {
if (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { }
else { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; }
});
failMsg = '\n推送失败的书源将用红色字体标注!';
}
alert(`批量推送书源到「阅读APP」\n共计: ${RuleSources.length}\n成功: ${okData.length}\n失败: ${RuleSources.length - okData.length}${failMsg}`);
}
else {
alert(`批量推送书源到「阅读APP」成功!\n共计: ${RuleSources.length}`);
}
}
else {
alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`);
}
}).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\n${err}`); });
thisNode.setAttribute('class', '');
})();
return;
case 'pull':
showTab('书源列表');
(async () => {
await HttpGet(`/getSources`).then(json => {
if (json.isSuccess) {
$('#RuleList').innerHTML = ''
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data));
RuleSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
alert(`成功拉取 ${RuleSources.length} 条书源`);
}
else {
alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`);
}
}).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\n${err}`); });
thisNode.setAttribute('class', '');
})();
return;
case 'editor':
if ($('#RuleJsonString').value == '') break;
try {
json2rule(JSON.parse($('#RuleJsonString').value));
todo();
} catch (error) {
console.log(error);
alert(error);
}
break;
case 'conver':
showTab('编辑书源');
$('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4);
break;
case 'initial':
$$('.rules textarea').forEach(item => { item.value = '' });
todo();
break;
case 'undo':
undo()
break;
case 'redo':
redo()
break;
case 'debug':
showTab('调试书源');
let wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\d+$/, (port) => (parseInt(port) + 1));
let DebugInfos = $('#DebugConsole');
function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }
let saveRule = [rule2json()];
HttpPost(`/saveSources`, saveRule).then(sResult => {
if (sResult.isSuccess) {
let sKey = DebugKey.value ? DebugKey.value : '我的';
$('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
let ws = new WebSocket(`${wsOrigin}/sourceDebug`);
ws.onopen = () => {
ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
};
ws.onmessage = (msg) => {
DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data);
if (msg.data == 'finish') setRule(saveRule[0]);
};
ws.onerror = (err) => {
throw `${err.data}`;
}
ws.onclose = () => {
thisNode.setAttribute('class', '');
DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`);
}
} else throw `${sResult.errorMsg}`;
}).catch(err => {
DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`);
thisNode.setAttribute('class', '');
});
return;
case 'accept':
(async () => {
let saveRule = [rule2json()];
await HttpPost(`/saveSources`, saveRule).then(json => {
alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
setRule(saveRule[0]);
}).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\n${err}`); });
thisNode.setAttribute('class', '');
})();
return;
default:
}
setTimeout(() => { thisNode.setAttribute('class', ''); }, 500);
});
$('#DebugKey').addEventListener('keydown', e => {
if (e.keyCode == 13) {
let clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent("click", true, false);
$('#debug').dispatchEvent(clickEvent);
}
});
// 列表规则更改事件
$('#RuleList').addEventListener('click', e => {
let editRule = null;
if (e.target && e.target.getAttribute('name') == 'rule') {
editRule = rule2json();
json2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id));
} else return;
if (editRule.bookSourceUrl == '') return;
if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, '');
setRule(editRule);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
});
// 处理列表按钮事件
$('.tab3>.titlebar').addEventListener('click', e => {
let thisNode = e.target;
if (thisNode.nodeName != 'BUTTON') return;
switch (thisNode.id) {
case 'Import':
let fileImport = document.createElement('input');
fileImport.type = 'file';
fileImport.accept = '.json';
fileImport.addEventListener('change', () => {
let file = fileImport.files[0];
let reader = new FileReader();
reader.onloadend = function (evt) {
if (evt.target.readyState == FileReader.DONE) {
let fileText = evt.target.result;
try {
let fileJson = JSON.parse(fileText);
let newSources = [];
newSources.push(...fileJson);
if (window.confirm(`如何处理导入的书源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) {
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = newSources));
$('#RuleList').innerHTML = ''
RuleSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
}
else {
newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));
RuleSources.push(...newSources);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
newSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
}
alert(`成功导入 ${newSources.length} 条书源`);
}
catch (err) {
alert(`导入书源文件失败!\n${err}`);
}
}
};
reader.readAsText(file);
}, false);
fileImport.click();
break;
case 'Export':
let fileExport = document.createElement('a');
fileExport.download = `Rules${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3').replace(/:/g, '')}.json`;
let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json" });
fileExport.href = window.URL.createObjectURL(myBlob);
fileExport.click();
break;
case 'Delete':
let selectRule = $('#RuleList input:checked');
if (!selectRule) {
alert(`没有书源被选中!`);
return;
}
if (confirm(`确定要删除选定书源吗?\n(同时删除APP内书源)`)) {
let selectRuleUrl = selectRule.id;
let deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的书源
let laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl)); // 提取待留下的书源
HttpPost(`/deleteSources`, deleteSources).then(json => {
if (json.isSuccess) {
let selectNode = document.getElementById(selectRuleUrl).parentNode;
selectNode.parentNode.removeChild(selectNode);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources));
if ($('#bookSourceUrl').value == selectRuleUrl) {
$$('.rules textarea').forEach(item => { item.value = '' });
todo();
}
console.log(deleteSources);
console.log(`以上书源已删除!`)
}
}).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\n${err}`); });
}
break;
case 'ClrAll':
if (confirm(`确定要清空当前书源列表吗?\n(不会删除APP内书源)`)) {
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = []));
$('#RuleList').innerHTML = ''
}
break;
default:
}
});

@ -1,7 +1,21 @@
package io.legado.app package io.legado.app
import android.app.Application import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.data.AppDatabase import io.legado.app.data.AppDatabase
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import java.util.*
class App : Application() { class App : Application() {
@ -15,9 +29,94 @@ class App : Application() {
private set private set
} }
private var versionCode = 0
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
INSTANCE = this INSTANCE = this
db = AppDatabase.createDatabase(INSTANCE) db = AppDatabase.createDatabase(INSTANCE)
versionCode = try {
packageManager.getPackageInfo(packageName, 0).versionCode
} catch (e: PackageManager.NameNotFoundException) {
0
}
initNightTheme()
if (!ThemeStore.isConfigured(this, versionCode)) {
upThemeStore()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannelId()
}
}
fun initNightTheme() {
if (getPrefBoolean("isNightTheme", false)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
}
/**
* 更新主题
*/
fun upThemeStore() {
if (getPrefBoolean("isNightTheme", false)) {
ThemeStore.editTheme(this)
.primaryColor(getPrefInt("colorPrimaryNight", resources.getColor(R.color.md_grey_800)))
.accentColor(getPrefInt("colorAccentNight", resources.getColor(R.color.md_pink_800)))
.backgroundColor(getPrefInt("colorBackgroundNight", resources.getColor(R.color.md_grey_800)))
.apply()
} else {
ThemeStore.editTheme(this)
.primaryColor(getPrefInt("colorPrimary", resources.getColor(R.color.md_grey_100)))
.accentColor(getPrefInt("colorAccent", resources.getColor(R.color.md_pink_600)))
.backgroundColor(getPrefInt("colorBackground", resources.getColor(R.color.md_grey_100)))
.apply()
}
}
/**
* 创建通知ID
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannelId() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel(
channelIdDownload,
getString(R.string.download_offline),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel
downloadChannel.enableLights(false)
downloadChannel.enableVibration(false)
downloadChannel.setSound(null, null)
//用唯一的ID创建渠道对象
val readAloudChannel = NotificationChannel(
channelIdReadAloud,
getString(R.string.read_aloud),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel
readAloudChannel.enableLights(false)
readAloudChannel.enableVibration(false)
readAloudChannel.setSound(null, null)
//用唯一的ID创建渠道对象
val webChannel = NotificationChannel(
channelIdWeb,
getString(R.string.web_service),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel
webChannel.enableLights(false)
webChannel.enableVibration(false)
webChannel.setSound(null, null)
//向notification manager 提交channel
notificationManager.createNotificationChannels(Arrays.asList(downloadChannel, readAloudChannel, webChannel))
} }
} }

@ -1,25 +1,28 @@
package io.legado.app.base package io.legado.app.base
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView import io.legado.app.R
import io.legado.app.lib.theme.ColorUtil
import io.legado.app.lib.theme.MaterialValueHelper
import io.legado.app.lib.theme.ThemeStore
import java.util.*
abstract class BaseActivity<BD : ViewDataBinding, VM : ViewModel?> : AppCompatActivity() { abstract class BaseActivity<VM : ViewModel> : AppCompatActivity() {
protected lateinit var dataBinding: BD
private set
protected abstract val viewModel: VM protected abstract val viewModel: VM
protected abstract val layoutID: Int protected abstract val layoutID: Int
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
initTheme()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
dataBinding = DataBindingUtil.setContentView(this, layoutID) setContentView(layoutID)
onViewModelCreated(viewModel, savedInstanceState) onViewModelCreated(viewModel, savedInstanceState)
} }
@ -27,6 +30,52 @@ abstract class BaseActivity<BD : ViewDataBinding, VM : ViewModel?> : AppCompatAc
} }
/**
* 设置MENU图标颜色
*/
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val primaryTextColor = MaterialValueHelper
.getPrimaryTextColor(this, ColorUtil.isColorLight(ThemeStore.primaryColor(this)))
for (i in 0 until menu.size()) {
val drawable = menu.getItem(i).icon
if (drawable != null) {
drawable.mutate()
drawable.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_ATOP)
}
}
return super.onCreateOptionsMenu(menu)
}
@SuppressLint("PrivateApi")
override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean {
if (menu != null) {
//展开菜单显示图标
if (menu.javaClass.simpleName.equals("MenuBuilder", ignoreCase = true)) {
try {
var method = menu.javaClass.getDeclaredMethod("setOptionalIconsVisible", java.lang.Boolean.TYPE)
method.isAccessible = true
method.invoke(menu, true)
method = menu.javaClass.getDeclaredMethod("getNonActionItems")
val menuItems = method.invoke(menu) as ArrayList<MenuItem>
if (menuItems.isNotEmpty()) {
for (menuItem in menuItems) {
val drawable = menuItem.icon
if (drawable != null) {
drawable.mutate()
drawable.setColorFilter(
resources.getColor(R.color.tv_text_default),
PorterDuff.Mode.SRC_ATOP
)
}
}
}
} catch (ignored: Exception) {
}
}
}
return super.onMenuOpened(featureId, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem?): Boolean {
item?.let { item?.let {
if (it.itemId == android.R.id.home) { if (it.itemId == android.R.id.home) {
@ -40,4 +89,12 @@ abstract class BaseActivity<BD : ViewDataBinding, VM : ViewModel?> : AppCompatAc
open fun onCompatOptionsItemSelected(item: MenuItem): Boolean { open fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
return true return true
} }
protected fun initTheme() {
if (ColorUtil.isColorLight(ThemeStore.primaryColor(this))) {
setTheme(R.style.CAppTheme)
} else {
setTheme(R.style.CAppThemeBarDark)
}
}
} }

@ -1,117 +0,0 @@
package io.legado.app.lib.theme;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Build;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public final class ATH {
@SuppressLint("CommitPrefEdits")
public static boolean didThemeValuesChange(@NonNull Context context, long since) {
return ThemeStore.isConfigured(context) && ThemeStore.prefs(context).getLong(ThemeStore.VALUES_CHANGED, -1) > since;
}
public static void setStatusbarColorAuto(Activity activity) {
setStatusbarColor(activity, ThemeStore.statusBarColor(activity));
}
public static void setStatusbarColor(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.getWindow().setStatusBarColor(color);
setLightStatusbarAuto(activity, color);
}
}
public static void setLightStatusbarAuto(Activity activity, int bgColor) {
setLightStatusbar(activity, ColorUtil.isColorLight(bgColor));
}
public static void setLightStatusbar(Activity activity, boolean enabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final View decorView = activity.getWindow().getDecorView();
final int systemUiVisibility = decorView.getSystemUiVisibility();
if (enabled) {
decorView.setSystemUiVisibility(systemUiVisibility | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
} else {
decorView.setSystemUiVisibility(systemUiVisibility & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
}
public static void setLightNavigationbar(Activity activity, boolean enabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
final View decorView = activity.getWindow().getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
if (enabled) {
systemUiVisibility |= SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
} else {
systemUiVisibility &= ~SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
decorView.setSystemUiVisibility(systemUiVisibility);
}
}
public static void setLightNavigationbarAuto(Activity activity, int bgColor) {
setLightNavigationbar(activity, ColorUtil.isColorLight(bgColor));
}
public static void setNavigationbarColorAuto(Activity activity) {
setNavigationbarColor(activity, ThemeStore.navigationBarColor(activity));
}
public static void setNavigationbarColor(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.getWindow().setNavigationBarColor(color);
setLightNavigationbarAuto(activity, color);
}
}
public static void setTaskDescriptionColorAuto(@NonNull Activity activity) {
setTaskDescriptionColor(activity, ThemeStore.primaryColor(activity));
}
public static void setTaskDescriptionColor(@NonNull Activity activity, @ColorInt int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Task description requires fully opaque color
color = ColorUtil.stripAlpha(color);
// Sets color of entry in the system recents page
activity.setTaskDescription(new ActivityManager.TaskDescription((String) activity.getTitle(), null, color));
}
}
public static void setTint(@NonNull View view, @ColorInt int color) {
TintHelper.setTintAuto(view, color, false);
}
public static void setBackgroundTint(@NonNull View view, @ColorInt int color) {
TintHelper.setTintAuto(view, color, true);
}
public static void setAlertDialogTint(@NonNull AlertDialog dialog) {
ColorStateList colorStateList = Selector.colorBuild()
.setDefaultColor(ThemeStore.accentColor(dialog.getContext()))
.setPressedColor(ColorUtil.darkenColor(ThemeStore.accentColor(dialog.getContext())))
.create();
if (dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE) != null) {
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList);
}
if (dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE) != null) {
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList);
}
}
private ATH() {
}
}

@ -0,0 +1,116 @@
package io.legado.app.lib.theme
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.view.View
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
/**
* @author Karim Abou Zeid (kabouzeid)
*/
object ATH {
@SuppressLint("CommitPrefEdits")
fun didThemeValuesChange(context: Context, since: Long): Boolean {
return ThemeStore.isConfigured(context) && ThemeStore.prefs(context).getLong(
ThemeStore.VALUES_CHANGED,
-1
) > since
}
fun setStatusbarColorAuto(activity: Activity) {
setStatusbarColor(activity, ThemeStore.statusBarColor(activity))
}
fun setStatusbarColor(activity: Activity, color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.statusBarColor = color
setLightStatusbarAuto(activity, color)
}
}
fun setLightStatusbarAuto(activity: Activity, bgColor: Int) {
setLightStatusbar(activity, ColorUtil.isColorLight(bgColor))
}
fun setLightStatusbar(activity: Activity, enabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val decorView = activity.window.decorView
val systemUiVisibility = decorView.systemUiVisibility
if (enabled) {
decorView.systemUiVisibility = systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
decorView.systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
}
}
fun setLightNavigationbar(activity: Activity, enabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val decorView = activity.window.decorView
var systemUiVisibility = decorView.systemUiVisibility
if (enabled) {
systemUiVisibility = systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
systemUiVisibility = systemUiVisibility and SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
}
decorView.systemUiVisibility = systemUiVisibility
}
}
fun setLightNavigationbarAuto(activity: Activity, bgColor: Int) {
setLightNavigationbar(activity, ColorUtil.isColorLight(bgColor))
}
fun setNavigationbarColorAuto(activity: Activity) {
setNavigationbarColor(activity, ThemeStore.navigationBarColor(activity))
}
fun setNavigationbarColor(activity: Activity, color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.navigationBarColor = color
setLightNavigationbarAuto(activity, color)
}
}
fun setTaskDescriptionColorAuto(activity: Activity) {
setTaskDescriptionColor(activity, ThemeStore.primaryColor(activity))
}
fun setTaskDescriptionColor(activity: Activity, @ColorInt color: Int) {
val color1: Int
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Task description requires fully opaque color
color1 = ColorUtil.stripAlpha(color)
// Sets color of entry in the system recents page
activity.setTaskDescription(ActivityManager.TaskDescription(activity.title as String, null, color1))
}
}
fun setTint(view: View, @ColorInt color: Int) {
TintHelper.setTintAuto(view, color, false)
}
fun setBackgroundTint(view: View, @ColorInt color: Int) {
TintHelper.setTintAuto(view, color, true)
}
fun setAlertDialogTint(dialog: AlertDialog): AlertDialog {
val colorStateList = Selector.colorBuild()
.setDefaultColor(ThemeStore.accentColor(dialog.context))
.setPressedColor(ColorUtil.darkenColor(ThemeStore.accentColor(dialog.context)))
.create()
if (dialog.getButton(AlertDialog.BUTTON_NEGATIVE) != null) {
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList)
}
if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList)
}
return dialog
}
}

@ -1,31 +0,0 @@
package io.legado.app.lib.theme;
import android.content.Context;
import android.content.res.TypedArray;
import androidx.annotation.AttrRes;
/**
* @author Aidan Follestad (afollestad)
*/
public final class ATHUtil {
public static boolean isWindowBackgroundDark(Context context) {
return !ColorUtil.isColorLight(ATHUtil.resolveColor(context, android.R.attr.windowBackground));
}
public static int resolveColor(Context context, @AttrRes int attr) {
return resolveColor(context, attr, 0);
}
public static int resolveColor(Context context, @AttrRes int attr, int fallback) {
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
try {
return a.getColor(0, fallback);
} finally {
a.recycle();
}
}
private ATHUtil() {
}
}

@ -0,0 +1,24 @@
package io.legado.app.lib.theme
import android.content.Context
import androidx.annotation.AttrRes
/**
* @author Aidan Follestad (afollestad)
*/
object ATHUtil {
fun isWindowBackgroundDark(context: Context): Boolean {
return !ColorUtil.isColorLight(ATHUtil.resolveColor(context, android.R.attr.windowBackground))
}
@JvmOverloads
fun resolveColor(context: Context, @AttrRes attr: Int, fallback: Int = 0): Int {
val a = context.theme.obtainStyledAttributes(intArrayOf(attr))
try {
return a.getColor(0, fallback)
} finally {
a.recycle()
}
}
}

@ -1,80 +0,0 @@
package io.legado.app.lib.theme;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
@SuppressWarnings({"unused", "WeakerAccess"})
public class ColorUtil {
public static String intToString(int intColor) {
return String.format("#%06X", 0xFFFFFF & intColor);
}
public static int stripAlpha(@ColorInt int color) {
return 0xff000000 | color;
}
@ColorInt
public static int shiftColor(@ColorInt int color, @FloatRange(from = 0.0f, to = 2.0f) float by) {
if (by == 1f) return color;
int alpha = Color.alpha(color);
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
hsv[2] *= by; // value component
return (alpha << 24) + (0x00ffffff & Color.HSVToColor(hsv));
}
@ColorInt
public static int darkenColor(@ColorInt int color) {
return shiftColor(color, 0.9f);
}
@ColorInt
public static int lightenColor(@ColorInt int color) {
return shiftColor(color, 1.1f);
}
public static boolean isColorLight(@ColorInt int color) {
final double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return darkness < 0.4;
}
@ColorInt
public static int invertColor(@ColorInt int color) {
final int r = 255 - Color.red(color);
final int g = 255 - Color.green(color);
final int b = 255 - Color.blue(color);
return Color.argb(Color.alpha(color), r, g, b);
}
@ColorInt
public static int adjustAlpha(@ColorInt int color, @FloatRange(from = 0.0, to = 1.0) float factor) {
int alpha = Math.round(Color.alpha(color) * factor);
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
return Color.argb(alpha, red, green, blue);
}
@ColorInt
public static int withAlpha(@ColorInt int baseColor, @FloatRange(from = 0.0, to = 1.0) float alpha) {
int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24;
int rgb = 0x00ffffff & baseColor;
return a + rgb;
}
/**
* Taken from CollapsingToolbarLayout's CollapsingTextHelper class.
*/
public static int blendColors(int color1, int color2, @FloatRange(from = 0.0, to = 1.0) float ratio) {
final float inverseRatio = 1f - ratio;
float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
return Color.argb((int) a, (int) r, (int) g, (int) b);
}
}

@ -0,0 +1,79 @@
package io.legado.app.lib.theme
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
object ColorUtil {
fun intToString(intColor: Int): String {
return String.format("#%06X", 0xFFFFFF and intColor)
}
fun stripAlpha(@ColorInt color: Int): Int {
return -0x1000000 or color
}
@ColorInt
fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int {
if (by == 1f) return color
val alpha = Color.alpha(color)
val hsv = FloatArray(3)
Color.colorToHSV(color, hsv)
hsv[2] *= by // value component
return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv))
}
@ColorInt
fun darkenColor(@ColorInt color: Int): Int {
return shiftColor(color, 0.9f)
}
@ColorInt
fun lightenColor(@ColorInt color: Int): Int {
return shiftColor(color, 1.1f)
}
fun isColorLight(@ColorInt color: Int): Boolean {
val darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
return darkness < 0.4
}
@ColorInt
fun invertColor(@ColorInt color: Int): Int {
val r = 255 - Color.red(color)
val g = 255 - Color.green(color)
val b = 255 - Color.blue(color)
return Color.argb(Color.alpha(color), r, g, b)
}
@ColorInt
fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int {
val alpha = Math.round(Color.alpha(color) * factor)
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
return Color.argb(alpha, red, green, blue)
}
@ColorInt
fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int {
val a = Math.min(255, Math.max(0, (alpha * 255).toInt())) shl 24
val rgb = 0x00ffffff and baseColor
return a + rgb
}
/**
* Taken from CollapsingToolbarLayout's CollapsingTextHelper class.
*/
fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int {
val inverseRatio = 1f - ratio
val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio
val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio
val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio
val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio
return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
}
}

@ -1,28 +0,0 @@
package io.legado.app.lib.theme;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import androidx.annotation.ColorInt;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public final class DrawableUtil {
public static TransitionDrawable createTransitionDrawable(@ColorInt int startColor, @ColorInt int endColor) {
return createTransitionDrawable(new ColorDrawable(startColor), new ColorDrawable(endColor));
}
public static TransitionDrawable createTransitionDrawable(Drawable start, Drawable end) {
final Drawable[] drawables = new Drawable[2];
drawables[0] = start;
drawables[1] = end;
return new TransitionDrawable(drawables);
}
private DrawableUtil() {
}
}

@ -0,0 +1,25 @@
package io.legado.app.lib.theme
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.TransitionDrawable
import androidx.annotation.ColorInt
/**
* @author Karim Abou Zeid (kabouzeid)
*/
object DrawableUtil {
fun createTransitionDrawable(@ColorInt startColor: Int, @ColorInt endColor: Int): TransitionDrawable {
return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor))
}
fun createTransitionDrawable(start: Drawable, end: Drawable): TransitionDrawable {
val drawables = arrayOfNulls<Drawable>(2)
drawables[0] = start
drawables[1] = end
return TransitionDrawable(drawables)
}
}

@ -1,52 +0,0 @@
package io.legado.app.lib.theme;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;
import io.legado.app.R;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public final class MaterialValueHelper {
@SuppressLint("PrivateResource")
@ColorInt
public static int getPrimaryTextColor(final Context context, boolean dark) {
if (dark) {
return ContextCompat.getColor(context, R.color.primary_text_default_material_light);
}
return ContextCompat.getColor(context, R.color.primary_text_default_material_dark);
}
@SuppressLint("PrivateResource")
@ColorInt
public static int getSecondaryTextColor(final Context context, boolean dark) {
if (dark) {
return ContextCompat.getColor(context, R.color.secondary_text_default_material_light);
}
return ContextCompat.getColor(context, R.color.secondary_text_default_material_dark);
}
@SuppressLint("PrivateResource")
@ColorInt
public static int getPrimaryDisabledTextColor(final Context context, boolean dark) {
if (dark) {
return ContextCompat.getColor(context, R.color.primary_text_disabled_material_light);
}
return ContextCompat.getColor(context, R.color.primary_text_disabled_material_dark);
}
@SuppressLint("PrivateResource")
@ColorInt
public static int getSecondaryDisabledTextColor(final Context context, boolean dark) {
if (dark) {
return ContextCompat.getColor(context, R.color.secondary_text_disabled_material_light);
}
return ContextCompat.getColor(context, R.color.secondary_text_disabled_material_dark);
}
private MaterialValueHelper() {
}
}

@ -0,0 +1,45 @@
package io.legado.app.lib.theme
import android.annotation.SuppressLint
import android.content.Context
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import io.legado.app.R
/**
* @author Karim Abou Zeid (kabouzeid)
*/
object MaterialValueHelper {
@SuppressLint("PrivateResource")
@ColorInt
fun getPrimaryTextColor(context: Context, dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(context, R.color.primary_text_default_material_light)
} else ContextCompat.getColor(context, R.color.primary_text_default_material_dark)
}
@SuppressLint("PrivateResource")
@ColorInt
fun getSecondaryTextColor(context: Context, dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(context, R.color.secondary_text_default_material_light)
} else ContextCompat.getColor(context, R.color.secondary_text_default_material_dark)
}
@SuppressLint("PrivateResource")
@ColorInt
fun getPrimaryDisabledTextColor(context: Context, dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(context, R.color.primary_text_disabled_material_light)
} else ContextCompat.getColor(context, R.color.primary_text_disabled_material_dark)
}
@SuppressLint("PrivateResource")
@ColorInt
fun getSecondaryDisabledTextColor(context: Context, dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(context, R.color.secondary_text_disabled_material_light)
} else ContextCompat.getColor(context, R.color.secondary_text_disabled_material_dark)
}
}

@ -1,55 +0,0 @@
package io.legado.app.lib.theme;
import android.content.res.ColorStateList;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import com.google.android.material.internal.NavigationMenuView;
import com.google.android.material.navigation.NavigationView;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public final class NavigationViewUtil {
public static void setItemIconColors(@NonNull NavigationView navigationView, @ColorInt int normalColor, @ColorInt int selectedColor) {
final ColorStateList iconSl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_checked},
new int[]{android.R.attr.state_checked}
},
new int[]{
normalColor,
selectedColor
});
navigationView.setItemIconTintList(iconSl);
}
public static void setItemTextColors(@NonNull NavigationView navigationView, @ColorInt int normalColor, @ColorInt int selectedColor) {
final ColorStateList textSl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_checked},
new int[]{android.R.attr.state_checked}
},
new int[]{
normalColor,
selectedColor
});
navigationView.setItemTextColor(textSl);
}
/**
* 去掉navigationView的滚动条
* @param navigationView NavigationView
*/
public static void disableScrollbar(NavigationView navigationView) {
if (navigationView != null) {
NavigationMenuView navigationMenuView = (NavigationMenuView) navigationView.getChildAt(0);
if (navigationMenuView != null) {
navigationMenuView.setVerticalScrollBarEnabled(false);
}
}
}
private NavigationViewUtil() {
}
}

@ -0,0 +1,38 @@
package io.legado.app.lib.theme
import android.content.res.ColorStateList
import androidx.annotation.ColorInt
import com.google.android.material.internal.NavigationMenuView
import com.google.android.material.navigation.NavigationView
/**
* @author Karim Abou Zeid (kabouzeid)
*/
object NavigationViewUtil {
fun setItemIconColors(navigationView: NavigationView, @ColorInt normalColor: Int, @ColorInt selectedColor: Int) {
val iconSl = ColorStateList(
arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)),
intArrayOf(normalColor, selectedColor)
)
navigationView.itemIconTintList = iconSl
}
fun setItemTextColors(navigationView: NavigationView, @ColorInt normalColor: Int, @ColorInt selectedColor: Int) {
val textSl = ColorStateList(
arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)),
intArrayOf(normalColor, selectedColor)
)
navigationView.itemTextColor = textSl
}
/**
* 去掉navigationView的滚动条
* @param navigationView NavigationView
*/
fun disableScrollbar(navigationView: NavigationView?) {
navigationView ?: return
val navigationMenuView = navigationView.getChildAt(0) as? NavigationMenuView
navigationMenuView?.isVerticalScrollBarEnabled = false
}
}

@ -1,430 +0,0 @@
package io.legado.app.lib.theme;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import androidx.annotation.ColorInt;
import androidx.annotation.Dimension;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.core.content.ContextCompat;
public class Selector {
public static ShapeSelector shapeBuild() {
return new ShapeSelector();
}
public static ColorSelector colorBuild() {
return new ColorSelector();
}
public static DrawableSelector drawableBuild() {
return new DrawableSelector();
}
/**
* 形状ShapeSelector
*
* @author hjy
* created at 2017/12/11 22:26
*/
public static final class ShapeSelector {
@IntDef({GradientDrawable.RECTANGLE, GradientDrawable.OVAL,
GradientDrawable.LINE, GradientDrawable.RING})
private @interface Shape {
}
private int mShape; //the shape of background
private int mDefaultBgColor; //default background color
private int mDisabledBgColor; //state_enabled = false
private int mPressedBgColor; //state_pressed = true
private int mSelectedBgColor; //state_selected = true
private int mFocusedBgColor; //state_focused = true
private int mCheckedBgColor; //state_checked = true
private int mStrokeWidth; //stroke width in pixel
private int mDefaultStrokeColor; //default stroke color
private int mDisabledStrokeColor; //state_enabled = false
private int mPressedStrokeColor; //state_pressed = true
private int mSelectedStrokeColor; //state_selected = true
private int mFocusedStrokeColor; //state_focused = true
private int mCheckedStrokeColor; //state_checked = true
private int mCornerRadius; //corner radius
private boolean hasSetDisabledBgColor = false;
private boolean hasSetPressedBgColor = false;
private boolean hasSetSelectedBgColor = false;
private boolean hasSetFocusedBgColor = false;
private boolean hasSetCheckedBgColor = false;
private boolean hasSetDisabledStrokeColor = false;
private boolean hasSetPressedStrokeColor = false;
private boolean hasSetSelectedStrokeColor = false;
private boolean hasSetFocusedStrokeColor = false;
private boolean hasSetCheckedStrokeColor = false;
public ShapeSelector() {
//initialize default values
mShape = GradientDrawable.RECTANGLE;
mDefaultBgColor = Color.TRANSPARENT;
mDisabledBgColor = Color.TRANSPARENT;
mPressedBgColor = Color.TRANSPARENT;
mSelectedBgColor = Color.TRANSPARENT;
mFocusedBgColor = Color.TRANSPARENT;
mStrokeWidth = 0;
mDefaultStrokeColor = Color.TRANSPARENT;
mDisabledStrokeColor = Color.TRANSPARENT;
mPressedStrokeColor = Color.TRANSPARENT;
mSelectedStrokeColor = Color.TRANSPARENT;
mFocusedStrokeColor = Color.TRANSPARENT;
mCornerRadius = 0;
}
public ShapeSelector setShape(@Shape int shape) {
mShape = shape;
return this;
}
public ShapeSelector setDefaultBgColor(@ColorInt int color) {
mDefaultBgColor = color;
if (!hasSetDisabledBgColor)
mDisabledBgColor = color;
if (!hasSetPressedBgColor)
mPressedBgColor = color;
if (!hasSetSelectedBgColor)
mSelectedBgColor = color;
if (!hasSetFocusedBgColor)
mFocusedBgColor = color;
return this;
}
public ShapeSelector setDisabledBgColor(@ColorInt int color) {
mDisabledBgColor = color;
hasSetDisabledBgColor = true;
return this;
}
public ShapeSelector setPressedBgColor(@ColorInt int color) {
mPressedBgColor = color;
hasSetPressedBgColor = true;
return this;
}
public ShapeSelector setSelectedBgColor(@ColorInt int color) {
mSelectedBgColor = color;
hasSetSelectedBgColor = true;
return this;
}
public ShapeSelector setFocusedBgColor(@ColorInt int color) {
mFocusedBgColor = color;
hasSetPressedBgColor = true;
return this;
}
public ShapeSelector setCheckedBgColor(@ColorInt int color) {
mCheckedBgColor = color;
hasSetCheckedBgColor = true;
return this;
}
public ShapeSelector setStrokeWidth(@Dimension int width) {
mStrokeWidth = width;
return this;
}
public ShapeSelector setDefaultStrokeColor(@ColorInt int color) {
mDefaultStrokeColor = color;
if (!hasSetDisabledStrokeColor)
mDisabledStrokeColor = color;
if (!hasSetPressedStrokeColor)
mPressedStrokeColor = color;
if (!hasSetSelectedStrokeColor)
mSelectedStrokeColor = color;
if (!hasSetFocusedStrokeColor)
mFocusedStrokeColor = color;
return this;
}
public ShapeSelector setDisabledStrokeColor(@ColorInt int color) {
mDisabledStrokeColor = color;
hasSetDisabledStrokeColor = true;
return this;
}
public ShapeSelector setPressedStrokeColor(@ColorInt int color) {
mPressedStrokeColor = color;
hasSetPressedStrokeColor = true;
return this;
}
public ShapeSelector setSelectedStrokeColor(@ColorInt int color) {
mSelectedStrokeColor = color;
hasSetSelectedStrokeColor = true;
return this;
}
public ShapeSelector setCheckedStrokeColor(@ColorInt int color) {
mCheckedStrokeColor = color;
hasSetCheckedStrokeColor = true;
return this;
}
public ShapeSelector setFocusedStrokeColor(@ColorInt int color) {
mFocusedStrokeColor = color;
hasSetFocusedStrokeColor = true;
return this;
}
public ShapeSelector setCornerRadius(@Dimension int radius) {
mCornerRadius = radius;
return this;
}
public StateListDrawable create() {
StateListDrawable selector = new StateListDrawable();
//enabled = false
if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) {
GradientDrawable disabledShape = getItemShape(mShape, mCornerRadius,
mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor);
selector.addState(new int[]{-android.R.attr.state_enabled}, disabledShape);
}
//pressed = true
if (hasSetPressedBgColor || hasSetPressedStrokeColor) {
GradientDrawable pressedShape = getItemShape(mShape, mCornerRadius,
mPressedBgColor, mStrokeWidth, mPressedStrokeColor);
selector.addState(new int[]{android.R.attr.state_pressed}, pressedShape);
}
//selected = true
if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) {
GradientDrawable selectedShape = getItemShape(mShape, mCornerRadius,
mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor);
selector.addState(new int[]{android.R.attr.state_selected}, selectedShape);
}
//focused = true
if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) {
GradientDrawable focusedShape = getItemShape(mShape, mCornerRadius,
mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor);
selector.addState(new int[]{android.R.attr.state_focused}, focusedShape);
}
//checked = true
if (hasSetCheckedBgColor || hasSetCheckedStrokeColor) {
GradientDrawable checkedShape = getItemShape(mShape, mCornerRadius,
mCheckedBgColor, mStrokeWidth, mCheckedStrokeColor);
selector.addState(new int[]{android.R.attr.state_checked}, checkedShape);
}
//default
GradientDrawable defaultShape = getItemShape(mShape, mCornerRadius,
mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor);
selector.addState(new int[]{}, defaultShape);
return selector;
}
private GradientDrawable getItemShape(int shape, int cornerRadius,
int solidColor, int strokeWidth, int strokeColor) {
GradientDrawable drawable = new GradientDrawable();
drawable.setShape(shape);
drawable.setStroke(strokeWidth, strokeColor);
drawable.setCornerRadius(cornerRadius);
drawable.setColor(solidColor);
return drawable;
}
}
/**
* 资源DrawableSelector
*
* @author hjy
* created at 2017/12/11 22:34
*/
public static final class DrawableSelector {
private Drawable mDefaultDrawable;
private Drawable mDisabledDrawable;
private Drawable mPressedDrawable;
private Drawable mSelectedDrawable;
private Drawable mFocusedDrawable;
private boolean hasSetDisabledDrawable = false;
private boolean hasSetPressedDrawable = false;
private boolean hasSetSelectedDrawable = false;
private boolean hasSetFocusedDrawable = false;
private DrawableSelector() {
mDefaultDrawable = new ColorDrawable(Color.TRANSPARENT);
}
public DrawableSelector setDefaultDrawable(Drawable drawable) {
mDefaultDrawable = drawable;
if (!hasSetDisabledDrawable)
mDisabledDrawable = drawable;
if (!hasSetPressedDrawable)
mPressedDrawable = drawable;
if (!hasSetSelectedDrawable)
mSelectedDrawable = drawable;
if (!hasSetFocusedDrawable)
mFocusedDrawable = drawable;
return this;
}
public DrawableSelector setDisabledDrawable(Drawable drawable) {
mDisabledDrawable = drawable;
hasSetDisabledDrawable = true;
return this;
}
public DrawableSelector setPressedDrawable(Drawable drawable) {
mPressedDrawable = drawable;
hasSetPressedDrawable = true;
return this;
}
public DrawableSelector setSelectedDrawable(Drawable drawable) {
mSelectedDrawable = drawable;
hasSetSelectedDrawable = true;
return this;
}
public DrawableSelector setFocusedDrawable(Drawable drawable) {
mFocusedDrawable = drawable;
hasSetFocusedDrawable = true;
return this;
}
public StateListDrawable create() {
StateListDrawable selector = new StateListDrawable();
if (hasSetDisabledDrawable)
selector.addState(new int[]{-android.R.attr.state_enabled}, mDisabledDrawable);
if (hasSetPressedDrawable)
selector.addState(new int[]{android.R.attr.state_pressed}, mPressedDrawable);
if (hasSetSelectedDrawable)
selector.addState(new int[]{android.R.attr.state_selected}, mSelectedDrawable);
if (hasSetFocusedDrawable)
selector.addState(new int[]{android.R.attr.state_focused}, mFocusedDrawable);
selector.addState(new int[]{}, mDefaultDrawable);
return selector;
}
public DrawableSelector setDefaultDrawable(Context context, @DrawableRes int drawableRes) {
return setDefaultDrawable(ContextCompat.getDrawable(context, drawableRes));
}
public DrawableSelector setDisabledDrawable(Context context, @DrawableRes int drawableRes) {
return setDisabledDrawable(ContextCompat.getDrawable(context, drawableRes));
}
public DrawableSelector setPressedDrawable(Context context, @DrawableRes int drawableRes) {
return setPressedDrawable(ContextCompat.getDrawable(context, drawableRes));
}
public DrawableSelector setSelectedDrawable(Context context, @DrawableRes int drawableRes) {
return setSelectedDrawable(ContextCompat.getDrawable(context, drawableRes));
}
public DrawableSelector setFocusedDrawable(Context context, @DrawableRes int drawableRes) {
return setFocusedDrawable(ContextCompat.getDrawable(context, drawableRes));
}
}
/**
* 颜色ColorSelector
*
* @author hjy
* created at 2017/12/11 22:26
*/
public static final class ColorSelector {
private int mDefaultColor;
private int mDisabledColor;
private int mPressedColor;
private int mSelectedColor;
private int mFocusedColor;
private int mCheckedColor;
private boolean hasSetDisabledColor = false;
private boolean hasSetPressedColor = false;
private boolean hasSetSelectedColor = false;
private boolean hasSetFocusedColor = false;
private boolean hasSetCheckedColor = false;
private ColorSelector() {
mDefaultColor = Color.BLACK;
mDisabledColor = Color.GRAY;
mPressedColor = Color.BLACK;
mSelectedColor = Color.BLACK;
mFocusedColor = Color.BLACK;
}
public ColorSelector setDefaultColor(@ColorInt int color) {
mDefaultColor = color;
if (!hasSetDisabledColor)
mDisabledColor = color;
if (!hasSetPressedColor)
mPressedColor = color;
if (!hasSetSelectedColor)
mSelectedColor = color;
if (!hasSetFocusedColor)
mFocusedColor = color;
return this;
}
public ColorSelector setDisabledColor(@ColorInt int color) {
mDisabledColor = color;
hasSetDisabledColor = true;
return this;
}
public ColorSelector setPressedColor(@ColorInt int color) {
mPressedColor = color;
hasSetPressedColor = true;
return this;
}
public ColorSelector setSelectedColor(@ColorInt int color) {
mSelectedColor = color;
hasSetSelectedColor = true;
return this;
}
public ColorSelector setFocusedColor(@ColorInt int color) {
mFocusedColor = color;
hasSetFocusedColor = true;
return this;
}
public ColorSelector setCheckedColor(@ColorInt int color) {
mCheckedColor = color;
hasSetCheckedColor = true;
return this;
}
public ColorStateList create() {
int[] colors = new int[]{
hasSetDisabledColor ? mDisabledColor : mDefaultColor,
hasSetPressedColor ? mPressedColor : mDefaultColor,
hasSetSelectedColor ? mSelectedColor : mDefaultColor,
hasSetFocusedColor ? mFocusedColor : mDefaultColor,
hasSetCheckedColor ? mCheckedColor : mDefaultColor,
mDefaultColor
};
int[][] states = new int[6][];
states[0] = new int[]{-android.R.attr.state_enabled};
states[1] = new int[]{android.R.attr.state_pressed};
states[2] = new int[]{android.R.attr.state_selected};
states[3] = new int[]{android.R.attr.state_focused};
states[4] = new int[]{android.R.attr.state_checked};
states[5] = new int[]{};
return new ColorStateList(states, colors);
}
}
}

@ -0,0 +1,443 @@
package io.legado.app.lib.theme
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
import androidx.annotation.ColorInt
import androidx.annotation.Dimension
import androidx.annotation.DrawableRes
import androidx.annotation.IntDef
import androidx.core.content.ContextCompat
object Selector {
fun shapeBuild(): ShapeSelector {
return ShapeSelector()
}
fun colorBuild(): ColorSelector {
return ColorSelector()
}
fun drawableBuild(): DrawableSelector {
return DrawableSelector()
}
/**
* 形状ShapeSelector
*
* @author hjy
* created at 2017/12/11 22:26
*/
class ShapeSelector {
private var mShape: Int = 0 //the shape of background
private var mDefaultBgColor: Int = 0 //default background color
private var mDisabledBgColor: Int = 0 //state_enabled = false
private var mPressedBgColor: Int = 0 //state_pressed = true
private var mSelectedBgColor: Int = 0 //state_selected = true
private var mFocusedBgColor: Int = 0 //state_focused = true
private var mCheckedBgColor: Int = 0 //state_checked = true
private var mStrokeWidth: Int = 0 //stroke width in pixel
private var mDefaultStrokeColor: Int = 0 //default stroke color
private var mDisabledStrokeColor: Int = 0 //state_enabled = false
private var mPressedStrokeColor: Int = 0 //state_pressed = true
private var mSelectedStrokeColor: Int = 0 //state_selected = true
private var mFocusedStrokeColor: Int = 0 //state_focused = true
private var mCheckedStrokeColor: Int = 0 //state_checked = true
private var mCornerRadius: Int = 0 //corner radius
private var hasSetDisabledBgColor = false
private var hasSetPressedBgColor = false
private var hasSetSelectedBgColor = false
private val hasSetFocusedBgColor = false
private var hasSetCheckedBgColor = false
private var hasSetDisabledStrokeColor = false
private var hasSetPressedStrokeColor = false
private var hasSetSelectedStrokeColor = false
private var hasSetFocusedStrokeColor = false
private var hasSetCheckedStrokeColor = false
@IntDef(GradientDrawable.RECTANGLE, GradientDrawable.OVAL, GradientDrawable.LINE, GradientDrawable.RING)
private annotation class Shape
init {
//initialize default values
mShape = GradientDrawable.RECTANGLE
mDefaultBgColor = Color.TRANSPARENT
mDisabledBgColor = Color.TRANSPARENT
mPressedBgColor = Color.TRANSPARENT
mSelectedBgColor = Color.TRANSPARENT
mFocusedBgColor = Color.TRANSPARENT
mStrokeWidth = 0
mDefaultStrokeColor = Color.TRANSPARENT
mDisabledStrokeColor = Color.TRANSPARENT
mPressedStrokeColor = Color.TRANSPARENT
mSelectedStrokeColor = Color.TRANSPARENT
mFocusedStrokeColor = Color.TRANSPARENT
mCornerRadius = 0
}
fun setShape(@Shape shape: Int): ShapeSelector {
mShape = shape
return this
}
fun setDefaultBgColor(@ColorInt color: Int): ShapeSelector {
mDefaultBgColor = color
if (!hasSetDisabledBgColor)
mDisabledBgColor = color
if (!hasSetPressedBgColor)
mPressedBgColor = color
if (!hasSetSelectedBgColor)
mSelectedBgColor = color
if (!hasSetFocusedBgColor)
mFocusedBgColor = color
return this
}
fun setDisabledBgColor(@ColorInt color: Int): ShapeSelector {
mDisabledBgColor = color
hasSetDisabledBgColor = true
return this
}
fun setPressedBgColor(@ColorInt color: Int): ShapeSelector {
mPressedBgColor = color
hasSetPressedBgColor = true
return this
}
fun setSelectedBgColor(@ColorInt color: Int): ShapeSelector {
mSelectedBgColor = color
hasSetSelectedBgColor = true
return this
}
fun setFocusedBgColor(@ColorInt color: Int): ShapeSelector {
mFocusedBgColor = color
hasSetPressedBgColor = true
return this
}
fun setCheckedBgColor(@ColorInt color: Int): ShapeSelector {
mCheckedBgColor = color
hasSetCheckedBgColor = true
return this
}
fun setStrokeWidth(@Dimension width: Int): ShapeSelector {
mStrokeWidth = width
return this
}
fun setDefaultStrokeColor(@ColorInt color: Int): ShapeSelector {
mDefaultStrokeColor = color
if (!hasSetDisabledStrokeColor)
mDisabledStrokeColor = color
if (!hasSetPressedStrokeColor)
mPressedStrokeColor = color
if (!hasSetSelectedStrokeColor)
mSelectedStrokeColor = color
if (!hasSetFocusedStrokeColor)
mFocusedStrokeColor = color
return this
}
fun setDisabledStrokeColor(@ColorInt color: Int): ShapeSelector {
mDisabledStrokeColor = color
hasSetDisabledStrokeColor = true
return this
}
fun setPressedStrokeColor(@ColorInt color: Int): ShapeSelector {
mPressedStrokeColor = color
hasSetPressedStrokeColor = true
return this
}
fun setSelectedStrokeColor(@ColorInt color: Int): ShapeSelector {
mSelectedStrokeColor = color
hasSetSelectedStrokeColor = true
return this
}
fun setCheckedStrokeColor(@ColorInt color: Int): ShapeSelector {
mCheckedStrokeColor = color
hasSetCheckedStrokeColor = true
return this
}
fun setFocusedStrokeColor(@ColorInt color: Int): ShapeSelector {
mFocusedStrokeColor = color
hasSetFocusedStrokeColor = true
return this
}
fun setCornerRadius(@Dimension radius: Int): ShapeSelector {
mCornerRadius = radius
return this
}
fun create(): StateListDrawable {
val selector = StateListDrawable()
//enabled = false
if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) {
val disabledShape = getItemShape(
mShape, mCornerRadius,
mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor
)
selector.addState(intArrayOf(-android.R.attr.state_enabled), disabledShape)
}
//pressed = true
if (hasSetPressedBgColor || hasSetPressedStrokeColor) {
val pressedShape = getItemShape(
mShape, mCornerRadius,
mPressedBgColor, mStrokeWidth, mPressedStrokeColor
)
selector.addState(intArrayOf(android.R.attr.state_pressed), pressedShape)
}
//selected = true
if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) {
val selectedShape = getItemShape(
mShape, mCornerRadius,
mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor
)
selector.addState(intArrayOf(android.R.attr.state_selected), selectedShape)
}
//focused = true
if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) {
val focusedShape = getItemShape(
mShape, mCornerRadius,
mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor
)
selector.addState(intArrayOf(android.R.attr.state_focused), focusedShape)
}
//checked = true
if (hasSetCheckedBgColor || hasSetCheckedStrokeColor) {
val checkedShape = getItemShape(
mShape, mCornerRadius,
mCheckedBgColor, mStrokeWidth, mCheckedStrokeColor
)
selector.addState(intArrayOf(android.R.attr.state_checked), checkedShape)
}
//default
val defaultShape = getItemShape(
mShape, mCornerRadius,
mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor
)
selector.addState(intArrayOf(), defaultShape)
return selector
}
private fun getItemShape(
shape: Int, cornerRadius: Int,
solidColor: Int, strokeWidth: Int, strokeColor: Int
): GradientDrawable {
val drawable = GradientDrawable()
drawable.shape = shape
drawable.setStroke(strokeWidth, strokeColor)
drawable.cornerRadius = cornerRadius.toFloat()
drawable.setColor(solidColor)
return drawable
}
}
/**
* 资源DrawableSelector
*
* @author hjy
* created at 2017/12/11 22:34
*/
class DrawableSelector constructor() {
private var mDefaultDrawable: Drawable? = null
private var mDisabledDrawable: Drawable? = null
private var mPressedDrawable: Drawable? = null
private var mSelectedDrawable: Drawable? = null
private var mFocusedDrawable: Drawable? = null
private var hasSetDisabledDrawable = false
private var hasSetPressedDrawable = false
private var hasSetSelectedDrawable = false
private var hasSetFocusedDrawable = false
init {
mDefaultDrawable = ColorDrawable(Color.TRANSPARENT)
}
fun setDefaultDrawable(drawable: Drawable?): DrawableSelector {
mDefaultDrawable = drawable
if (!hasSetDisabledDrawable)
mDisabledDrawable = drawable
if (!hasSetPressedDrawable)
mPressedDrawable = drawable
if (!hasSetSelectedDrawable)
mSelectedDrawable = drawable
if (!hasSetFocusedDrawable)
mFocusedDrawable = drawable
return this
}
fun setDisabledDrawable(drawable: Drawable?): DrawableSelector {
mDisabledDrawable = drawable
hasSetDisabledDrawable = true
return this
}
fun setPressedDrawable(drawable: Drawable?): DrawableSelector {
mPressedDrawable = drawable
hasSetPressedDrawable = true
return this
}
fun setSelectedDrawable(drawable: Drawable?): DrawableSelector {
mSelectedDrawable = drawable
hasSetSelectedDrawable = true
return this
}
fun setFocusedDrawable(drawable: Drawable?): DrawableSelector {
mFocusedDrawable = drawable
hasSetFocusedDrawable = true
return this
}
fun create(): StateListDrawable {
val selector = StateListDrawable()
if (hasSetDisabledDrawable)
selector.addState(intArrayOf(-android.R.attr.state_enabled), mDisabledDrawable)
if (hasSetPressedDrawable)
selector.addState(intArrayOf(android.R.attr.state_pressed), mPressedDrawable)
if (hasSetSelectedDrawable)
selector.addState(intArrayOf(android.R.attr.state_selected), mSelectedDrawable)
if (hasSetFocusedDrawable)
selector.addState(intArrayOf(android.R.attr.state_focused), mFocusedDrawable)
selector.addState(intArrayOf(), mDefaultDrawable)
return selector
}
fun setDefaultDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector {
return setDefaultDrawable(ContextCompat.getDrawable(context, drawableRes))
}
fun setDisabledDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector {
return setDisabledDrawable(ContextCompat.getDrawable(context, drawableRes))
}
fun setPressedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector {
return setPressedDrawable(ContextCompat.getDrawable(context, drawableRes))
}
fun setSelectedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector {
return setSelectedDrawable(ContextCompat.getDrawable(context, drawableRes))
}
fun setFocusedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector {
return setFocusedDrawable(ContextCompat.getDrawable(context, drawableRes))
}
}
/**
* 颜色ColorSelector
*
* @author hjy
* created at 2017/12/11 22:26
*/
class ColorSelector constructor() {
private var mDefaultColor: Int = 0
private var mDisabledColor: Int = 0
private var mPressedColor: Int = 0
private var mSelectedColor: Int = 0
private var mFocusedColor: Int = 0
private var mCheckedColor: Int = 0
private var hasSetDisabledColor = false
private var hasSetPressedColor = false
private var hasSetSelectedColor = false
private var hasSetFocusedColor = false
private var hasSetCheckedColor = false
init {
mDefaultColor = Color.BLACK
mDisabledColor = Color.GRAY
mPressedColor = Color.BLACK
mSelectedColor = Color.BLACK
mFocusedColor = Color.BLACK
}
fun setDefaultColor(@ColorInt color: Int): ColorSelector {
mDefaultColor = color
if (!hasSetDisabledColor)
mDisabledColor = color
if (!hasSetPressedColor)
mPressedColor = color
if (!hasSetSelectedColor)
mSelectedColor = color
if (!hasSetFocusedColor)
mFocusedColor = color
return this
}
fun setDisabledColor(@ColorInt color: Int): ColorSelector {
mDisabledColor = color
hasSetDisabledColor = true
return this
}
fun setPressedColor(@ColorInt color: Int): ColorSelector {
mPressedColor = color
hasSetPressedColor = true
return this
}
fun setSelectedColor(@ColorInt color: Int): ColorSelector {
mSelectedColor = color
hasSetSelectedColor = true
return this
}
fun setFocusedColor(@ColorInt color: Int): ColorSelector {
mFocusedColor = color
hasSetFocusedColor = true
return this
}
fun setCheckedColor(@ColorInt color: Int): ColorSelector {
mCheckedColor = color
hasSetCheckedColor = true
return this
}
fun create(): ColorStateList {
val colors = intArrayOf(
if (hasSetDisabledColor) mDisabledColor else mDefaultColor,
if (hasSetPressedColor) mPressedColor else mDefaultColor,
if (hasSetSelectedColor) mSelectedColor else mDefaultColor,
if (hasSetFocusedColor) mFocusedColor else mDefaultColor,
if (hasSetCheckedColor) mCheckedColor else mDefaultColor,
mDefaultColor
)
val states = arrayOfNulls<IntArray>(6)
states[0] = intArrayOf(-android.R.attr.state_enabled)
states[1] = intArrayOf(android.R.attr.state_pressed)
states[2] = intArrayOf(android.R.attr.state_selected)
states[3] = intArrayOf(android.R.attr.state_focused)
states[4] = intArrayOf(android.R.attr.state_checked)
states[5] = intArrayOf()
return ColorStateList(states, colors)
}
}
}

@ -1,318 +0,0 @@
package io.legado.app.lib.theme;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import androidx.annotation.*;
import androidx.core.content.ContextCompat;
import io.legado.app.R;
/**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/
public final class ThemeStore implements ThemeStorePrefKeys, ThemeStoreInterface {
private final Context mContext;
private final SharedPreferences.Editor mEditor;
public static ThemeStore editTheme(@NonNull Context context) {
return new ThemeStore(context);
}
@SuppressLint("CommitPrefEdits")
private ThemeStore(@NonNull Context context) {
mContext = context;
mEditor = prefs(context).edit();
}
@Override
public ThemeStore primaryColor(@ColorInt int color) {
mEditor.putInt(KEY_PRIMARY_COLOR, color);
if (autoGeneratePrimaryDark(mContext))
primaryColorDark(ColorUtil.darkenColor(color));
return this;
}
@Override
public ThemeStore primaryColorRes(@ColorRes int colorRes) {
return primaryColor(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore primaryColorAttr(@AttrRes int colorAttr) {
return primaryColor(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore primaryColorDark(@ColorInt int color) {
mEditor.putInt(KEY_PRIMARY_COLOR_DARK, color);
return this;
}
@Override
public ThemeStore primaryColorDarkRes(@ColorRes int colorRes) {
return primaryColorDark(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore primaryColorDarkAttr(@AttrRes int colorAttr) {
return primaryColorDark(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore accentColor(@ColorInt int color) {
mEditor.putInt(KEY_ACCENT_COLOR, color);
return this;
}
@Override
public ThemeStore accentColorRes(@ColorRes int colorRes) {
return accentColor(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore accentColorAttr(@AttrRes int colorAttr) {
return accentColor(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore statusBarColor(@ColorInt int color) {
mEditor.putInt(KEY_STATUS_BAR_COLOR, color);
return this;
}
@Override
public ThemeStore statusBarColorRes(@ColorRes int colorRes) {
return statusBarColor(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore statusBarColorAttr(@AttrRes int colorAttr) {
return statusBarColor(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore navigationBarColor(@ColorInt int color) {
mEditor.putInt(KEY_NAVIGATION_BAR_COLOR, color);
return this;
}
@Override
public ThemeStore navigationBarColorRes(@ColorRes int colorRes) {
return navigationBarColor(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore navigationBarColorAttr(@AttrRes int colorAttr) {
return navigationBarColor(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore textColorPrimary(@ColorInt int color) {
mEditor.putInt(KEY_TEXT_COLOR_PRIMARY, color);
return this;
}
@Override
public ThemeStore textColorPrimaryRes(@ColorRes int colorRes) {
return textColorPrimary(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore textColorPrimaryAttr(@AttrRes int colorAttr) {
return textColorPrimary(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore textColorPrimaryInverse(@ColorInt int color) {
mEditor.putInt(KEY_TEXT_COLOR_PRIMARY_INVERSE, color);
return this;
}
@Override
public ThemeStore textColorPrimaryInverseRes(@ColorRes int colorRes) {
return textColorPrimaryInverse(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore textColorPrimaryInverseAttr(@AttrRes int colorAttr) {
return textColorPrimaryInverse(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore textColorSecondary(@ColorInt int color) {
mEditor.putInt(KEY_TEXT_COLOR_SECONDARY, color);
return this;
}
@Override
public ThemeStore textColorSecondaryRes(@ColorRes int colorRes) {
return textColorSecondary(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore textColorSecondaryAttr(@AttrRes int colorAttr) {
return textColorSecondary(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore textColorSecondaryInverse(@ColorInt int color) {
mEditor.putInt(KEY_TEXT_COLOR_SECONDARY_INVERSE, color);
return this;
}
@Override
public ThemeStore textColorSecondaryInverseRes(@ColorRes int colorRes) {
return textColorSecondaryInverse(ContextCompat.getColor(mContext, colorRes));
}
@Override
public ThemeStore textColorSecondaryInverseAttr(@AttrRes int colorAttr) {
return textColorSecondaryInverse(ATHUtil.resolveColor(mContext, colorAttr));
}
@Override
public ThemeStore backgroundColor(int color) {
mEditor.putInt(KEY_BACKGROUND_COLOR, color);
return this;
}
@Override
public ThemeStore coloredStatusBar(boolean colored) {
mEditor.putBoolean(KEY_APPLY_PRIMARYDARK_STATUSBAR, colored);
return this;
}
@Override
public ThemeStore coloredNavigationBar(boolean applyToNavBar) {
mEditor.putBoolean(KEY_APPLY_PRIMARY_NAVBAR, applyToNavBar);
return this;
}
@Override
public ThemeStore autoGeneratePrimaryDark(boolean autoGenerate) {
mEditor.putBoolean(KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate);
return this;
}
// Commit method
@SuppressWarnings("unchecked")
@Override
public void apply() {
mEditor.putLong(VALUES_CHANGED, System.currentTimeMillis())
.putBoolean(IS_CONFIGURED_KEY, true)
.apply();
}
// Static getters
@CheckResult
@NonNull
protected static SharedPreferences prefs(@NonNull Context context) {
return context.getSharedPreferences(CONFIG_PREFS_KEY_DEFAULT, Context.MODE_PRIVATE);
}
public static void markChanged(@NonNull Context context) {
new ThemeStore(context).apply();
}
@CheckResult
@ColorInt
public static int primaryColor(@NonNull Context context) {
return prefs(context).getInt(KEY_PRIMARY_COLOR, ATHUtil.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64")));
}
@CheckResult
@ColorInt
public static int primaryColorDark(@NonNull Context context) {
return prefs(context).getInt(KEY_PRIMARY_COLOR_DARK, ATHUtil.resolveColor(context, R.attr.colorPrimaryDark, Color.parseColor("#37474F")));
}
@CheckResult
@ColorInt
public static int accentColor(@NonNull Context context) {
return prefs(context).getInt(KEY_ACCENT_COLOR, ATHUtil.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238")));
}
@CheckResult
@ColorInt
public static int statusBarColor(@NonNull Context context) {
if (!coloredStatusBar(context)) {
return Color.BLACK;
}
return prefs(context).getInt(KEY_STATUS_BAR_COLOR, primaryColorDark(context));
}
@CheckResult
@ColorInt
public static int navigationBarColor(@NonNull Context context) {
if (!coloredNavigationBar(context)) {
return Color.BLACK;
}
return prefs(context).getInt(KEY_NAVIGATION_BAR_COLOR, primaryColor(context));
}
@CheckResult
@ColorInt
public static int textColorPrimary(@NonNull Context context) {
return prefs(context).getInt(KEY_TEXT_COLOR_PRIMARY, ATHUtil.resolveColor(context, android.R.attr.textColorPrimary));
}
@CheckResult
@ColorInt
public static int textColorPrimaryInverse(@NonNull Context context) {
return prefs(context).getInt(KEY_TEXT_COLOR_PRIMARY_INVERSE, ATHUtil.resolveColor(context, android.R.attr.textColorPrimaryInverse));
}
@CheckResult
@ColorInt
public static int textColorSecondary(@NonNull Context context) {
return prefs(context).getInt(KEY_TEXT_COLOR_SECONDARY, ATHUtil.resolveColor(context, android.R.attr.textColorSecondary));
}
@CheckResult
@ColorInt
public static int textColorSecondaryInverse(@NonNull Context context) {
return prefs(context).getInt(KEY_TEXT_COLOR_SECONDARY_INVERSE, ATHUtil.resolveColor(context, android.R.attr.textColorSecondaryInverse));
}
@CheckResult
@ColorInt
public static int backgroundColor(@NonNull Context context) {
return prefs(context).getInt(KEY_BACKGROUND_COLOR, ATHUtil.resolveColor(context, android.R.attr.colorBackground));
}
@CheckResult
public static boolean coloredStatusBar(@NonNull Context context) {
return prefs(context).getBoolean(KEY_APPLY_PRIMARYDARK_STATUSBAR, true);
}
@CheckResult
public static boolean coloredNavigationBar(@NonNull Context context) {
return prefs(context).getBoolean(KEY_APPLY_PRIMARY_NAVBAR, false);
}
@CheckResult
public static boolean autoGeneratePrimaryDark(@NonNull Context context) {
return prefs(context).getBoolean(KEY_AUTO_GENERATE_PRIMARYDARK, true);
}
@CheckResult
public static boolean isConfigured(Context context) {
return prefs(context).getBoolean(IS_CONFIGURED_KEY, false);
}
@SuppressLint("CommitPrefEdits")
public static boolean isConfigured(Context context, @IntRange(from = 0, to = Integer.MAX_VALUE) int version) {
final SharedPreferences prefs = prefs(context);
final int lastVersion = prefs.getInt(IS_CONFIGURED_VERSION_KEY, -1);
if (version > lastVersion) {
prefs.edit().putInt(IS_CONFIGURED_VERSION_KEY, version).apply();
return false;
}
return true;
}
}

@ -0,0 +1,309 @@
package io.legado.app.lib.theme
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Color
import androidx.annotation.AttrRes
import androidx.annotation.CheckResult
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import io.legado.app.R
/**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/
class ThemeStore @SuppressLint("CommitPrefEdits")
private constructor(private val mContext: Context) : ThemeStorePrefKeys, ThemeStoreInterface {
private val mEditor: SharedPreferences.Editor
init {
mEditor = prefs(mContext).edit()
}
override fun primaryColor(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR, color)
if (autoGeneratePrimaryDark(mContext))
primaryColorDark(ColorUtil.darkenColor(color))
return this
}
override fun primaryColorRes(@ColorRes colorRes: Int): ThemeStore {
return primaryColor(ContextCompat.getColor(mContext, colorRes))
}
override fun primaryColorAttr(@AttrRes colorAttr: Int): ThemeStore {
return primaryColor(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun primaryColorDark(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK, color)
return this
}
override fun primaryColorDarkRes(@ColorRes colorRes: Int): ThemeStore {
return primaryColorDark(ContextCompat.getColor(mContext, colorRes))
}
override fun primaryColorDarkAttr(@AttrRes colorAttr: Int): ThemeStore {
return primaryColorDark(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun accentColor(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_ACCENT_COLOR, color)
return this
}
override fun accentColorRes(@ColorRes colorRes: Int): ThemeStore {
return accentColor(ContextCompat.getColor(mContext, colorRes))
}
override fun accentColorAttr(@AttrRes colorAttr: Int): ThemeStore {
return accentColor(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun statusBarColor(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, color)
return this
}
override fun statusBarColorRes(@ColorRes colorRes: Int): ThemeStore {
return statusBarColor(ContextCompat.getColor(mContext, colorRes))
}
override fun statusBarColorAttr(@AttrRes colorAttr: Int): ThemeStore {
return statusBarColor(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun navigationBarColor(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, color)
return this
}
override fun navigationBarColorRes(@ColorRes colorRes: Int): ThemeStore {
return navigationBarColor(ContextCompat.getColor(mContext, colorRes))
}
override fun navigationBarColorAttr(@AttrRes colorAttr: Int): ThemeStore {
return navigationBarColor(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun textColorPrimary(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY, color)
return this
}
override fun textColorPrimaryRes(@ColorRes colorRes: Int): ThemeStore {
return textColorPrimary(ContextCompat.getColor(mContext, colorRes))
}
override fun textColorPrimaryAttr(@AttrRes colorAttr: Int): ThemeStore {
return textColorPrimary(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun textColorPrimaryInverse(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY_INVERSE, color)
return this
}
override fun textColorPrimaryInverseRes(@ColorRes colorRes: Int): ThemeStore {
return textColorPrimaryInverse(ContextCompat.getColor(mContext, colorRes))
}
override fun textColorPrimaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore {
return textColorPrimaryInverse(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun textColorSecondary(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY, color)
return this
}
override fun textColorSecondaryRes(@ColorRes colorRes: Int): ThemeStore {
return textColorSecondary(ContextCompat.getColor(mContext, colorRes))
}
override fun textColorSecondaryAttr(@AttrRes colorAttr: Int): ThemeStore {
return textColorSecondary(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun textColorSecondaryInverse(@ColorInt color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY_INVERSE, color)
return this
}
override fun textColorSecondaryInverseRes(@ColorRes colorRes: Int): ThemeStore {
return textColorSecondaryInverse(ContextCompat.getColor(mContext, colorRes))
}
override fun textColorSecondaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore {
return textColorSecondaryInverse(ATHUtil.resolveColor(mContext, colorAttr))
}
override fun backgroundColor(color: Int): ThemeStore {
mEditor.putInt(ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, color)
return this
}
override fun coloredStatusBar(colored: Boolean): ThemeStore {
mEditor.putBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, colored)
return this
}
override fun coloredNavigationBar(applyToNavBar: Boolean): ThemeStore {
mEditor.putBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARY_NAVBAR, applyToNavBar)
return this
}
override fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore {
mEditor.putBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate)
return this
}
// Commit method
override fun apply() {
mEditor.putLong(ThemeStorePrefKeys.VALUES_CHANGED, System.currentTimeMillis())
.putBoolean(ThemeStorePrefKeys.IS_CONFIGURED_KEY, true)
.apply()
}
companion object {
fun editTheme(context: Context): ThemeStore {
return ThemeStore(context)
}
// Static getters
@CheckResult
protected fun prefs(context: Context): SharedPreferences {
return context.getSharedPreferences(ThemeStorePrefKeys.CONFIG_PREFS_KEY_DEFAULT, Context.MODE_PRIVATE)
}
fun markChanged(context: Context) {
ThemeStore(context).apply()
}
@CheckResult
@ColorInt
fun primaryColor(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_PRIMARY_COLOR,
ATHUtil.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))
)
}
@CheckResult
@ColorInt
fun primaryColorDark(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK,
ATHUtil.resolveColor(context, R.attr.colorPrimaryDark, Color.parseColor("#37474F"))
)
}
@CheckResult
@ColorInt
fun accentColor(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_ACCENT_COLOR,
ATHUtil.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238"))
)
}
@CheckResult
@ColorInt
fun statusBarColor(context: Context): Int {
return if (!coloredStatusBar(context)) {
Color.BLACK
} else prefs(context).getInt(ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, primaryColorDark(context))
}
@CheckResult
@ColorInt
fun navigationBarColor(context: Context): Int {
return if (!coloredNavigationBar(context)) {
Color.BLACK
} else prefs(context).getInt(ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, primaryColor(context))
}
@CheckResult
@ColorInt
fun textColorPrimary(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY,
ATHUtil.resolveColor(context, android.R.attr.textColorPrimary)
)
}
@CheckResult
@ColorInt
fun textColorPrimaryInverse(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY_INVERSE,
ATHUtil.resolveColor(context, android.R.attr.textColorPrimaryInverse)
)
}
@CheckResult
@ColorInt
fun textColorSecondary(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY,
ATHUtil.resolveColor(context, android.R.attr.textColorSecondary)
)
}
@CheckResult
@ColorInt
fun textColorSecondaryInverse(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY_INVERSE,
ATHUtil.resolveColor(context, android.R.attr.textColorSecondaryInverse)
)
}
@CheckResult
@ColorInt
fun backgroundColor(context: Context): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BACKGROUND_COLOR,
ATHUtil.resolveColor(context, android.R.attr.colorBackground)
)
}
@CheckResult
fun coloredStatusBar(context: Context): Boolean {
return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, true)
}
@CheckResult
fun coloredNavigationBar(context: Context): Boolean {
return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARY_NAVBAR, false)
}
@CheckResult
fun autoGeneratePrimaryDark(context: Context): Boolean {
return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, true)
}
@CheckResult
fun isConfigured(context: Context): Boolean {
return prefs(context).getBoolean(ThemeStorePrefKeys.IS_CONFIGURED_KEY, false)
}
@SuppressLint("CommitPrefEdits")
fun isConfigured(context: Context, version: Int): Boolean {
val prefs = prefs(context)
val lastVersion = prefs.getInt(ThemeStorePrefKeys.IS_CONFIGURED_VERSION_KEY, -1)
if (version > lastVersion) {
prefs.edit().putInt(ThemeStorePrefKeys.IS_CONFIGURED_VERSION_KEY, version).apply()
return false
}
return true
}
}
}

@ -1,92 +0,0 @@
package io.legado.app.lib.theme;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
/**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/
interface ThemeStoreInterface {
// Primary colors
ThemeStore primaryColor(@ColorInt int color);
ThemeStore primaryColorRes(@ColorRes int colorRes);
ThemeStore primaryColorAttr(@AttrRes int colorAttr);
ThemeStore autoGeneratePrimaryDark(boolean autoGenerate);
ThemeStore primaryColorDark(@ColorInt int color);
ThemeStore primaryColorDarkRes(@ColorRes int colorRes);
ThemeStore primaryColorDarkAttr(@AttrRes int colorAttr);
// Accent colors
ThemeStore accentColor(@ColorInt int color);
ThemeStore accentColorRes(@ColorRes int colorRes);
ThemeStore accentColorAttr(@AttrRes int colorAttr);
// Status bar color
ThemeStore statusBarColor(@ColorInt int color);
ThemeStore statusBarColorRes(@ColorRes int colorRes);
ThemeStore statusBarColorAttr(@AttrRes int colorAttr);
// Navigation bar color
ThemeStore navigationBarColor(@ColorInt int color);
ThemeStore navigationBarColorRes(@ColorRes int colorRes);
ThemeStore navigationBarColorAttr(@AttrRes int colorAttr);
// Primary text color
ThemeStore textColorPrimary(@ColorInt int color);
ThemeStore textColorPrimaryRes(@ColorRes int colorRes);
ThemeStore textColorPrimaryAttr(@AttrRes int colorAttr);
ThemeStore textColorPrimaryInverse(@ColorInt int color);
ThemeStore textColorPrimaryInverseRes(@ColorRes int colorRes);
ThemeStore textColorPrimaryInverseAttr(@AttrRes int colorAttr);
// Secondary text color
ThemeStore textColorSecondary(@ColorInt int color);
ThemeStore textColorSecondaryRes(@ColorRes int colorRes);
ThemeStore textColorSecondaryAttr(@AttrRes int colorAttr);
ThemeStore textColorSecondaryInverse(@ColorInt int color);
ThemeStore textColorSecondaryInverseRes(@ColorRes int colorRes);
ThemeStore textColorSecondaryInverseAttr(@AttrRes int colorAttr);
ThemeStore backgroundColor(@ColorInt int color);
// Toggle configurations
ThemeStore coloredStatusBar(boolean colored);
ThemeStore coloredNavigationBar(boolean applyToNavBar);
// Commit/apply
void apply();
}

@ -0,0 +1,92 @@
package io.legado.app.lib.theme
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
/**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/
internal interface ThemeStoreInterface {
// Primary colors
fun primaryColor(@ColorInt color: Int): ThemeStore
fun primaryColorRes(@ColorRes colorRes: Int): ThemeStore
fun primaryColorAttr(@AttrRes colorAttr: Int): ThemeStore
fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore
fun primaryColorDark(@ColorInt color: Int): ThemeStore
fun primaryColorDarkRes(@ColorRes colorRes: Int): ThemeStore
fun primaryColorDarkAttr(@AttrRes colorAttr: Int): ThemeStore
// Accent colors
fun accentColor(@ColorInt color: Int): ThemeStore
fun accentColorRes(@ColorRes colorRes: Int): ThemeStore
fun accentColorAttr(@AttrRes colorAttr: Int): ThemeStore
// Status bar color
fun statusBarColor(@ColorInt color: Int): ThemeStore
fun statusBarColorRes(@ColorRes colorRes: Int): ThemeStore
fun statusBarColorAttr(@AttrRes colorAttr: Int): ThemeStore
// Navigation bar color
fun navigationBarColor(@ColorInt color: Int): ThemeStore
fun navigationBarColorRes(@ColorRes colorRes: Int): ThemeStore
fun navigationBarColorAttr(@AttrRes colorAttr: Int): ThemeStore
// Primary text color
fun textColorPrimary(@ColorInt color: Int): ThemeStore
fun textColorPrimaryRes(@ColorRes colorRes: Int): ThemeStore
fun textColorPrimaryAttr(@AttrRes colorAttr: Int): ThemeStore
fun textColorPrimaryInverse(@ColorInt color: Int): ThemeStore
fun textColorPrimaryInverseRes(@ColorRes colorRes: Int): ThemeStore
fun textColorPrimaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore
// Secondary text color
fun textColorSecondary(@ColorInt color: Int): ThemeStore
fun textColorSecondaryRes(@ColorRes colorRes: Int): ThemeStore
fun textColorSecondaryAttr(@AttrRes colorAttr: Int): ThemeStore
fun textColorSecondaryInverse(@ColorInt color: Int): ThemeStore
fun textColorSecondaryInverseRes(@ColorRes colorRes: Int): ThemeStore
fun textColorSecondaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore
fun backgroundColor(@ColorInt color: Int): ThemeStore
// Toggle configurations
fun coloredStatusBar(colored: Boolean): ThemeStore
fun coloredNavigationBar(applyToNavBar: Boolean): ThemeStore
// Commit/apply
fun apply()
}

@ -1,29 +0,0 @@
package io.legado.app.lib.theme;
/**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/
interface ThemeStorePrefKeys {
String CONFIG_PREFS_KEY_DEFAULT = "app_themes";
String IS_CONFIGURED_KEY = "is_configured";
String IS_CONFIGURED_VERSION_KEY = "is_configured_version";
String VALUES_CHANGED = "values_changed";
String KEY_PRIMARY_COLOR = "primary_color";
String KEY_PRIMARY_COLOR_DARK = "primary_color_dark";
String KEY_ACCENT_COLOR = "accent_color";
String KEY_STATUS_BAR_COLOR = "status_bar_color";
String KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color";
String KEY_TEXT_COLOR_PRIMARY = "text_color_primary";
String KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse";
String KEY_TEXT_COLOR_SECONDARY = "text_color_secondary";
String KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse";
String KEY_BACKGROUND_COLOR = "backgroundColor";
String KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar";
String KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar";
String KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark";
}

@ -0,0 +1,31 @@
package io.legado.app.lib.theme
/**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/
internal interface ThemeStorePrefKeys {
companion object {
val CONFIG_PREFS_KEY_DEFAULT = "app_themes"
val IS_CONFIGURED_KEY = "is_configured"
val IS_CONFIGURED_VERSION_KEY = "is_configured_version"
val VALUES_CHANGED = "values_changed"
val KEY_PRIMARY_COLOR = "primary_color"
val KEY_PRIMARY_COLOR_DARK = "primary_color_dark"
val KEY_ACCENT_COLOR = "accent_color"
val KEY_STATUS_BAR_COLOR = "status_bar_color"
val KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color"
val KEY_TEXT_COLOR_PRIMARY = "text_color_primary"
val KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse"
val KEY_TEXT_COLOR_SECONDARY = "text_color_secondary"
val KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse"
val KEY_BACKGROUND_COLOR = "backgroundColor"
val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar"
val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar"
val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark"
}
}

@ -1,384 +0,0 @@
package io.legado.app.lib.theme;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.view.View;
import android.widget.*;
import androidx.annotation.CheckResult;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import io.legado.app.R;
import java.lang.reflect.Field;
/**
* @author afollestad, plusCubed
*/
public final class TintHelper {
@SuppressLint("PrivateResource")
@ColorInt
private static int getDefaultRippleColor(@NonNull Context context, boolean useDarkRipple) {
// Light ripple is actually translucent black, and vice versa
return ContextCompat.getColor(context, useDarkRipple ?
R.color.ripple_material_light : R.color.ripple_material_dark);
}
@NonNull
private static ColorStateList getDisabledColorStateList(@ColorInt int normal, @ColorInt int disabled) {
return new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled}
}, new int[]{
disabled,
normal
});
}
@SuppressWarnings("deprecation")
public static void setTintSelector(@NonNull View view, @ColorInt final int color, final boolean darker, final boolean useDarkTheme) {
final boolean isColorLight = ColorUtil.isColorLight(color);
final int disabled = ContextCompat.getColor(view.getContext(), useDarkTheme ? R.color.ate_button_disabled_dark : R.color.ate_button_disabled_light);
final int pressed = ColorUtil.shiftColor(color, darker ? 0.9f : 1.1f);
final int activated = ColorUtil.shiftColor(color, darker ? 1.1f : 0.9f);
final int rippleColor = getDefaultRippleColor(view.getContext(), isColorLight);
final int textColor = ContextCompat.getColor(view.getContext(), isColorLight ? R.color.ate_primary_text_light : R.color.ate_primary_text_dark);
final ColorStateList sl;
if (view instanceof Button) {
sl = getDisabledColorStateList(color, disabled);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
view.getBackground() instanceof RippleDrawable) {
RippleDrawable rd = (RippleDrawable) view.getBackground();
rd.setColor(ColorStateList.valueOf(rippleColor));
}
// Disabled text color state for buttons, may get overridden later by ATE tags
final Button button = (Button) view;
button.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(), useDarkTheme ? R.color.ate_button_text_disabled_dark : R.color.ate_button_text_disabled_light)));
} else if (view instanceof FloatingActionButton) {
// FloatingActionButton doesn't support disabled state?
sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_pressed},
new int[]{android.R.attr.state_pressed}
}, new int[]{
color,
pressed
});
final FloatingActionButton fab = (FloatingActionButton) view;
fab.setRippleColor(rippleColor);
fab.setBackgroundTintList(sl);
if (fab.getDrawable() != null)
fab.setImageDrawable(createTintedDrawable(fab.getDrawable(), textColor));
return;
} else {
sl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed},
new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}
},
new int[]{
disabled,
color,
pressed,
activated,
activated
}
);
}
Drawable drawable = view.getBackground();
if (drawable != null) {
drawable = createTintedDrawable(drawable, sl);
ViewUtil.setBackgroundCompat(view, drawable);
}
if (view instanceof TextView && !(view instanceof Button)) {
final TextView tv = (TextView) view;
tv.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(), isColorLight ? R.color.ate_text_disabled_light : R.color.ate_text_disabled_dark)));
}
}
public static void setTintAuto(final @NonNull View view, final @ColorInt int color,
boolean background) {
setTintAuto(view, color, background, ATHUtil.isWindowBackgroundDark(view.getContext()));
}
@SuppressWarnings("deprecation")
public static void setTintAuto(final @NonNull View view, final @ColorInt int color,
boolean background, final boolean isDark) {
if (!background) {
if (view instanceof RadioButton)
setTint((RadioButton) view, color, isDark);
else if (view instanceof SeekBar)
setTint((SeekBar) view, color, isDark);
else if (view instanceof ProgressBar)
setTint((ProgressBar) view, color);
else if (view instanceof AppCompatEditText)
setTint((AppCompatEditText) view, color, isDark);
else if (view instanceof CheckBox)
setTint((CheckBox) view, color, isDark);
else if (view instanceof ImageView)
setTint((ImageView) view, color);
else if (view instanceof Switch)
setTint((Switch) view, color, isDark);
else if (view instanceof SwitchCompat)
setTint((SwitchCompat) view, color, isDark);
else if (view instanceof SearchView) {
int iconIdS[] = new int[]{androidx.appcompat.R.id.search_button, androidx.appcompat.R.id.search_close_btn,};
for (int iconId : iconIdS) {
ImageView icon = view.findViewById(iconId);
if (icon != null) {
setTint(icon, color);
}
}
} else {
background = true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
!background && view.getBackground() instanceof RippleDrawable) {
// Ripples for the above views (e.g. when you tap and hold a switch or checkbox)
RippleDrawable rd = (RippleDrawable) view.getBackground();
@SuppressLint("PrivateResource") final int unchecked = ContextCompat.getColor(view.getContext(),
isDark ? R.color.ripple_material_dark : R.color.ripple_material_light);
final int checked = ColorUtil.adjustAlpha(color, 0.4f);
final ColorStateList sl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_activated, -android.R.attr.state_checked},
new int[]{android.R.attr.state_activated},
new int[]{android.R.attr.state_checked}
},
new int[]{
unchecked,
checked,
checked
}
);
rd.setColor(sl);
}
}
if (background) {
// Need to tint the background of a view
if (view instanceof FloatingActionButton || view instanceof Button) {
setTintSelector(view, color, false, isDark);
} else if (view.getBackground() != null) {
Drawable drawable = view.getBackground();
if (drawable != null) {
drawable = createTintedDrawable(drawable, color);
ViewUtil.setBackgroundCompat(view, drawable);
}
}
}
}
public static void setTint(@NonNull RadioButton radioButton, @ColorInt int color, boolean useDarker) {
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}
}, new int[]{
// Rdio button includes own alpha for disabled state
ColorUtil.stripAlpha(ContextCompat.getColor(radioButton.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light)),
ContextCompat.getColor(radioButton.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),
color
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
radioButton.setButtonTintList(sl);
} else {
Drawable d = createTintedDrawable(ContextCompat.getDrawable(radioButton.getContext(), R.drawable.abc_btn_radio_material), sl);
radioButton.setButtonDrawable(d);
}
}
public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color, boolean useDarker) {
final ColorStateList s1 = getDisabledColorStateList(color,
ContextCompat.getColor(seekBar.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
seekBar.setThumbTintList(s1);
seekBar.setProgressTintList(s1);
} else {
Drawable progressDrawable = createTintedDrawable(seekBar.getProgressDrawable(), s1);
seekBar.setProgressDrawable(progressDrawable);
Drawable thumbDrawable = createTintedDrawable(seekBar.getThumb(), s1);
seekBar.setThumb(thumbDrawable);
}
}
public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color) {
setTint(progressBar, color, false);
}
public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color, boolean skipIndeterminate) {
ColorStateList sl = ColorStateList.valueOf(color);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
progressBar.setProgressTintList(sl);
progressBar.setSecondaryProgressTintList(sl);
if (!skipIndeterminate)
progressBar.setIndeterminateTintList(sl);
} else {
PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN;
if (!skipIndeterminate && progressBar.getIndeterminateDrawable() != null)
progressBar.getIndeterminateDrawable().setColorFilter(color, mode);
if (progressBar.getProgressDrawable() != null)
progressBar.getProgressDrawable().setColorFilter(color, mode);
}
}
@SuppressLint("RestrictedApi")
public static void setTint(@NonNull AppCompatEditText editText, @ColorInt int color, boolean useDarker) {
final ColorStateList editTextColorStateList = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed, -android.R.attr.state_focused},
new int[]{}
}, new int[]{
ContextCompat.getColor(editText.getContext(), useDarker ? R.color.ate_text_disabled_dark : R.color.ate_text_disabled_light),
ContextCompat.getColor(editText.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),
color
});
editText.setSupportBackgroundTintList(editTextColorStateList);
setCursorTint(editText, color);
}
public static void setTint(@NonNull CheckBox box, @ColorInt int color, boolean useDarker) {
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}
}, new int[]{
ContextCompat.getColor(box.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light),
ContextCompat.getColor(box.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),
color
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
box.setButtonTintList(sl);
} else {
Drawable drawable = createTintedDrawable(ContextCompat.getDrawable(box.getContext(), R.drawable.abc_btn_check_material), sl);
box.setButtonDrawable(drawable);
}
}
public static void setTint(@NonNull ImageView image, @ColorInt int color) {
image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
private static Drawable modifySwitchDrawable(@NonNull Context context, @NonNull Drawable from, @ColorInt int tint, boolean thumb, boolean compatSwitch, boolean useDarker) {
if (useDarker) {
tint = ColorUtil.shiftColor(tint, 1.1f);
}
tint = ColorUtil.adjustAlpha(tint, (compatSwitch && !thumb) ? 0.5f : 1.0f);
int disabled;
int normal;
if (thumb) {
disabled = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_thumb_disabled_dark : R.color.ate_switch_thumb_disabled_light);
normal = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_thumb_normal_dark : R.color.ate_switch_thumb_normal_light);
} else {
disabled = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_track_disabled_dark : R.color.ate_switch_track_disabled_light);
normal = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_track_normal_dark : R.color.ate_switch_track_normal_light);
}
// Stock switch includes its own alpha
if (!compatSwitch) {
normal = ColorUtil.stripAlpha(normal);
}
final ColorStateList sl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, -android.R.attr.state_activated, -android.R.attr.state_checked},
new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}
},
new int[]{
disabled,
normal,
tint,
tint
}
);
return createTintedDrawable(from, sl);
}
public static void setTint(@NonNull Switch switchView, @ColorInt int color, boolean useDarker) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return;
if (switchView.getTrackDrawable() != null) {
switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getTrackDrawable(), color, false, false, useDarker));
}
if (switchView.getThumbDrawable() != null) {
switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getThumbDrawable(), color, true, false, useDarker));
}
}
public static void setTint(@NonNull SwitchCompat switchView, @ColorInt int color, boolean useDarker) {
if (switchView.getTrackDrawable() != null) {
switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getTrackDrawable(), color, false, true, useDarker));
}
if (switchView.getThumbDrawable() != null) {
switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getThumbDrawable(), color, true, true, useDarker));
}
}
// This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.
@CheckResult
@Nullable
public static Drawable createTintedDrawable(@Nullable Drawable drawable, @ColorInt int color) {
if (drawable == null) return null;
drawable = DrawableCompat.wrap(drawable.mutate());
DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN);
DrawableCompat.setTint(drawable, color);
return drawable;
}
// This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.
@CheckResult
@Nullable
public static Drawable createTintedDrawable(@Nullable Drawable drawable, @NonNull ColorStateList sl) {
if (drawable == null) return null;
drawable = DrawableCompat.wrap(drawable.mutate());
DrawableCompat.setTintList(drawable, sl);
return drawable;
}
public static void setCursorTint(@NonNull EditText editText, @ColorInt int color) {
try {
Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes");
fCursorDrawableRes.setAccessible(true);
int mCursorDrawableRes = fCursorDrawableRes.getInt(editText);
Field fEditor = TextView.class.getDeclaredField("mEditor");
fEditor.setAccessible(true);
Object editor = fEditor.get(editText);
Class<?> clazz = editor.getClass();
Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable");
fCursorDrawable.setAccessible(true);
Drawable[] drawables = new Drawable[2];
drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);
drawables[0] = createTintedDrawable(drawables[0], color);
drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);
drawables[1] = createTintedDrawable(drawables[1], color);
fCursorDrawable.set(editor, drawables);
} catch (Exception ignored) {
}
}
}

@ -0,0 +1,455 @@
package io.legado.app.lib.theme
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.graphics.drawable.RippleDrawable
import android.os.Build
import android.view.View
import android.widget.*
import androidx.annotation.CheckResult
import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import io.legado.app.R
/**
* @author afollestad, plusCubed
*/
object TintHelper {
@SuppressLint("PrivateResource")
@ColorInt
private fun getDefaultRippleColor(context: Context, useDarkRipple: Boolean): Int {
// Light ripple is actually translucent black, and vice versa
return ContextCompat.getColor(
context, if (useDarkRipple)
R.color.ripple_material_light
else
R.color.ripple_material_dark
)
}
private fun getDisabledColorStateList(@ColorInt normal: Int, @ColorInt disabled: Int): ColorStateList {
return ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_enabled)
), intArrayOf(disabled, normal)
)
}
fun setTintSelector(view: View, @ColorInt color: Int, darker: Boolean, useDarkTheme: Boolean) {
val isColorLight = ColorUtil.isColorLight(color)
val disabled = ContextCompat.getColor(
view.context,
if (useDarkTheme) R.color.ate_button_disabled_dark else R.color.ate_button_disabled_light
)
val pressed = ColorUtil.shiftColor(color, if (darker) 0.9f else 1.1f)
val activated = ColorUtil.shiftColor(color, if (darker) 1.1f else 0.9f)
val rippleColor = getDefaultRippleColor(view.context, isColorLight)
val textColor = ContextCompat.getColor(
view.context,
if (isColorLight) R.color.ate_primary_text_light else R.color.ate_primary_text_dark
)
val sl: ColorStateList
if (view is Button) {
sl = getDisabledColorStateList(color, disabled)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && view.getBackground() is RippleDrawable) {
val rd = view.getBackground() as RippleDrawable
rd.setColor(ColorStateList.valueOf(rippleColor))
}
// Disabled text color state for buttons, may get overridden later by ATE tags
view.setTextColor(
getDisabledColorStateList(
textColor,
ContextCompat.getColor(
view.getContext(),
if (useDarkTheme) R.color.ate_button_text_disabled_dark else R.color.ate_button_text_disabled_light
)
)
)
} else if (view is FloatingActionButton) {
// FloatingActionButton doesn't support disabled state?
sl = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_pressed),
intArrayOf(android.R.attr.state_pressed)
), intArrayOf(color, pressed)
)
view.rippleColor = rippleColor
view.backgroundTintList = sl
if (view.drawable != null)
view.setImageDrawable(createTintedDrawable(view.drawable, textColor))
return
} else {
sl = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked)
),
intArrayOf(disabled, color, pressed, activated, activated)
)
}
var drawable: Drawable? = view.background
if (drawable != null) {
drawable = createTintedDrawable(drawable, sl)
ViewUtil.setBackgroundCompat(view, drawable)
}
if (view is TextView && view !is Button) {
view.setTextColor(
getDisabledColorStateList(
textColor,
ContextCompat.getColor(
view.getContext(),
if (isColorLight) R.color.ate_text_disabled_light else R.color.ate_text_disabled_dark
)
)
)
}
}
fun setTintAuto(
view: View, @ColorInt color: Int,
background: Boolean
) {
setTintAuto(view, color, background, ATHUtil.isWindowBackgroundDark(view.context))
}
fun setTintAuto(
view: View, @ColorInt color: Int,
background: Boolean, isDark: Boolean
) {
var background = background
if (!background) {
if (view is RadioButton)
setTint(view, color, isDark)
else if (view is SeekBar)
setTint(view, color, isDark)
else if (view is ProgressBar)
setTint(view, color)
else if (view is AppCompatEditText)
setTint(view, color, isDark)
else if (view is CheckBox)
setTint(view, color, isDark)
else if (view is ImageView)
setTint(view, color)
else if (view is Switch)
setTint(view, color, isDark)
else if (view is SwitchCompat)
setTint(view, color, isDark)
else if (view is SearchView) {
val iconIdS =
intArrayOf(androidx.appcompat.R.id.search_button, androidx.appcompat.R.id.search_close_btn)
for (iconId in iconIdS) {
val icon = view.findViewById<ImageView>(iconId)
if (icon != null) {
setTint(icon, color)
}
}
} else {
background = true
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
!background && view.background is RippleDrawable
) {
// Ripples for the above views (e.g. when you tap and hold a switch or checkbox)
val rd = view.background as RippleDrawable
@SuppressLint("PrivateResource") val unchecked = ContextCompat.getColor(
view.context,
if (isDark) R.color.ripple_material_dark else R.color.ripple_material_light
)
val checked = ColorUtil.adjustAlpha(color, 0.4f)
val sl = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_activated, -android.R.attr.state_checked),
intArrayOf(android.R.attr.state_activated),
intArrayOf(android.R.attr.state_checked)
),
intArrayOf(unchecked, checked, checked)
)
rd.setColor(sl)
}
}
if (background) {
// Need to tint the background of a view
if (view is FloatingActionButton || view is Button) {
setTintSelector(view, color, false, isDark)
} else if (view.background != null) {
var drawable: Drawable? = view.background
if (drawable != null) {
drawable = createTintedDrawable(drawable, color)
ViewUtil.setBackgroundCompat(view, drawable)
}
}
}
}
fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) {
val sl = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked)
), intArrayOf(
// Rdio button includes own alpha for disabled state
ColorUtil.stripAlpha(
ContextCompat.getColor(
radioButton.context,
if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light
)
),
ContextCompat.getColor(
radioButton.context,
if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light
),
color
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
radioButton.buttonTintList = sl
} else {
val d = createTintedDrawable(
ContextCompat.getDrawable(radioButton.context, R.drawable.abc_btn_radio_material),
sl
)
radioButton.buttonDrawable = d
}
}
fun setTint(seekBar: SeekBar, @ColorInt color: Int, useDarker: Boolean) {
val s1 = getDisabledColorStateList(
color,
ContextCompat.getColor(
seekBar.context,
if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
seekBar.thumbTintList = s1
seekBar.progressTintList = s1
} else {
val progressDrawable = createTintedDrawable(seekBar.progressDrawable, s1)
seekBar.progressDrawable = progressDrawable
val thumbDrawable = createTintedDrawable(seekBar.thumb, s1)
seekBar.thumb = thumbDrawable
}
}
@JvmOverloads
fun setTint(progressBar: ProgressBar, @ColorInt color: Int, skipIndeterminate: Boolean = false) {
val sl = ColorStateList.valueOf(color)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
progressBar.progressTintList = sl
progressBar.secondaryProgressTintList = sl
if (!skipIndeterminate)
progressBar.indeterminateTintList = sl
} else {
val mode = PorterDuff.Mode.SRC_IN
if (!skipIndeterminate && progressBar.indeterminateDrawable != null)
progressBar.indeterminateDrawable.setColorFilter(color, mode)
if (progressBar.progressDrawable != null)
progressBar.progressDrawable.setColorFilter(color, mode)
}
}
@SuppressLint("RestrictedApi")
fun setTint(editText: AppCompatEditText, @ColorInt color: Int, useDarker: Boolean) {
val editTextColorStateList = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_pressed, -android.R.attr.state_focused),
intArrayOf()
),
intArrayOf(
ContextCompat.getColor(
editText.context,
if (useDarker) R.color.ate_text_disabled_dark else R.color.ate_text_disabled_light
),
ContextCompat.getColor(
editText.context,
if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light
),
color
)
)
editText.supportBackgroundTintList = editTextColorStateList
setCursorTint(editText, color)
}
fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) {
val sl = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked)
),
intArrayOf(
ContextCompat.getColor(
box.context,
if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light
),
ContextCompat.getColor(
box.context,
if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light
),
color
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
box.buttonTintList = sl
} else {
val drawable =
createTintedDrawable(ContextCompat.getDrawable(box.context, R.drawable.abc_btn_check_material), sl)
box.buttonDrawable = drawable
}
}
fun setTint(image: ImageView, @ColorInt color: Int) {
image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
}
private fun modifySwitchDrawable(
context: Context,
from: Drawable, @ColorInt tint: Int,
thumb: Boolean,
compatSwitch: Boolean,
useDarker: Boolean
): Drawable? {
var tint = tint
if (useDarker) {
tint = ColorUtil.shiftColor(tint, 1.1f)
}
tint = ColorUtil.adjustAlpha(tint, if (compatSwitch && !thumb) 0.5f else 1.0f)
val disabled: Int
var normal: Int
if (thumb) {
disabled = ContextCompat.getColor(
context,
if (useDarker) R.color.ate_switch_thumb_disabled_dark else R.color.ate_switch_thumb_disabled_light
)
normal = ContextCompat.getColor(
context,
if (useDarker) R.color.ate_switch_thumb_normal_dark else R.color.ate_switch_thumb_normal_light
)
} else {
disabled = ContextCompat.getColor(
context,
if (useDarker) R.color.ate_switch_track_disabled_dark else R.color.ate_switch_track_disabled_light
)
normal = ContextCompat.getColor(
context,
if (useDarker) R.color.ate_switch_track_normal_dark else R.color.ate_switch_track_normal_light
)
}
// Stock switch includes its own alpha
if (!compatSwitch) {
normal = ColorUtil.stripAlpha(normal)
}
val sl = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(
android.R.attr.state_enabled,
-android.R.attr.state_activated,
-android.R.attr.state_checked
),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated),
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked)
),
intArrayOf(disabled, normal, tint, tint)
)
return createTintedDrawable(from, sl)
}
fun setTint(switchView: Switch, @ColorInt color: Int, useDarker: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return
if (switchView.trackDrawable != null) {
switchView.trackDrawable = modifySwitchDrawable(
switchView.context,
switchView.trackDrawable, color, false, false, useDarker
)
}
if (switchView.thumbDrawable != null) {
switchView.thumbDrawable = modifySwitchDrawable(
switchView.context,
switchView.thumbDrawable, color, true, false, useDarker
)
}
}
fun setTint(switchView: SwitchCompat, @ColorInt color: Int, useDarker: Boolean) {
if (switchView.trackDrawable != null) {
switchView.trackDrawable = modifySwitchDrawable(
switchView.context,
switchView.trackDrawable, color, false, true, useDarker
)
}
if (switchView.thumbDrawable != null) {
switchView.thumbDrawable = modifySwitchDrawable(
switchView.context,
switchView.thumbDrawable, color, true, true, useDarker
)
}
}
// This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.
@CheckResult
fun createTintedDrawable(drawable: Drawable?, @ColorInt color: Int): Drawable? {
var drawable: Drawable? = drawable ?: return null
drawable = DrawableCompat.wrap(drawable!!.mutate())
DrawableCompat.setTintMode(drawable!!, PorterDuff.Mode.SRC_IN)
DrawableCompat.setTint(drawable, color)
return drawable
}
// This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.
@CheckResult
fun createTintedDrawable(drawable: Drawable?, sl: ColorStateList): Drawable? {
var drawable: Drawable? = drawable ?: return null
drawable = DrawableCompat.wrap(drawable!!.mutate())
DrawableCompat.setTintList(drawable!!, sl)
return drawable
}
fun setCursorTint(editText: EditText, @ColorInt color: Int) {
try {
val fCursorDrawableRes = TextView::class.java.getDeclaredField("mCursorDrawableRes")
fCursorDrawableRes.isAccessible = true
val mCursorDrawableRes = fCursorDrawableRes.getInt(editText)
val fEditor = TextView::class.java.getDeclaredField("mEditor")
fEditor.isAccessible = true
val editor = fEditor.get(editText)
val clazz = editor.javaClass
val fCursorDrawable = clazz.getDeclaredField("mCursorDrawable")
fCursorDrawable.isAccessible = true
val drawables = arrayOfNulls<Drawable>(2)
drawables[0] = ContextCompat.getDrawable(editText.context, mCursorDrawableRes)
drawables[0] = createTintedDrawable(drawables[0], color)
drawables[1] = ContextCompat.getDrawable(editText.context, mCursorDrawableRes)
drawables[1] = createTintedDrawable(drawables[1], color)
fCursorDrawable.set(editor, drawables)
} catch (ignored: Exception) {
}
}
}

@ -1,48 +0,0 @@
package io.legado.app.lib.theme;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public final class ViewUtil {
@SuppressWarnings("deprecation")
public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener) {
v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
}
@SuppressWarnings("deprecation")
public static void setBackgroundCompat(@NonNull View view, @Nullable Drawable drawable) {
view.setBackground(drawable);
}
public static TransitionDrawable setBackgroundTransition(@NonNull View view, @NonNull Drawable newDrawable) {
TransitionDrawable transition = DrawableUtil.createTransitionDrawable(view.getBackground(), newDrawable);
setBackgroundCompat(view, transition);
return transition;
}
public static TransitionDrawable setBackgroundColorTransition(@NonNull View view, @ColorInt int newColor) {
final Drawable oldColor = view.getBackground();
Drawable start = oldColor != null ? oldColor : new ColorDrawable(view.getSolidColor());
Drawable end = new ColorDrawable(newColor);
TransitionDrawable transition = DrawableUtil.createTransitionDrawable(start, end);
setBackgroundCompat(view, transition);
return transition;
}
private ViewUtil() {
}
}

@ -0,0 +1,41 @@
package io.legado.app.lib.theme
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.TransitionDrawable
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.ColorInt
/**
* @author Karim Abou Zeid (kabouzeid)
*/
object ViewUtil {
fun removeOnGlobalLayoutListener(v: View, listener: ViewTreeObserver.OnGlobalLayoutListener) {
v.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
fun setBackgroundCompat(view: View, drawable: Drawable?) {
view.background = drawable
}
fun setBackgroundTransition(view: View, newDrawable: Drawable): TransitionDrawable {
val transition = DrawableUtil.createTransitionDrawable(view.background, newDrawable)
setBackgroundCompat(view, transition)
return transition
}
fun setBackgroundColorTransition(view: View, @ColorInt newColor: Int): TransitionDrawable {
val oldColor = view.background
val start = oldColor ?: ColorDrawable(view.solidColor)
val end = ColorDrawable(newColor)
val transition = DrawableUtil.createTransitionDrawable(start, end)
setBackgroundCompat(view, transition)
return transition
}
}

@ -0,0 +1,36 @@
package io.legado.app.lib.theme.prefs
import android.content.Context
import android.os.Build
import android.preference.PreferenceCategory
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.annotation.RequiresApi
import io.legado.app.lib.theme.ThemeStore
class ATEPreferenceCategory : PreferenceCategory {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindView(view: View) {
super.onBindView(view)
if (view is TextView) {
view.setTextColor(ThemeStore.accentColor(view.getContext()))//设置title文本的颜色
}
}
}

@ -0,0 +1,52 @@
package io.legado.app.lib.theme.prefs
import android.content.Context
import android.os.Build
import android.preference.SwitchPreference
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.Switch
import androidx.annotation.RequiresApi
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
import java.util.*
class ATESwitchPreference : SwitchPreference {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindView(view: View) {
super.onBindView(view)
if (view is ViewGroup) {
val queue = LinkedList<ViewGroup>()
queue.add(view)
while (!queue.isEmpty()) {
val current = queue.removeFirst()
for (i in 0 until current.childCount) {
if (current.getChildAt(i) is Switch) {
ATH.setTint(current.getChildAt(i), ThemeStore.accentColor(view.getContext()))
return
} else if (current.getChildAt(i) is ViewGroup) {
queue.addLast(current.getChildAt(i) as ViewGroup)
}
}
}
}
}
}

@ -0,0 +1,95 @@
package io.legado.app.lib.theme.prefs
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog.Builder
import android.content.Context
import android.graphics.drawable.Drawable
import android.preference.ListPreference
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.ListAdapter
import io.legado.app.R
import java.util.*
class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) {
private val mEntryDrawables = ArrayList<Drawable>()
init {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0)
val drawables: Array<CharSequence>
try {
drawables = a.getTextArray(R.styleable.IconListPreference_icons)
} finally {
a.recycle()
}
for (drawable in drawables) {
val resId = context.resources.getIdentifier(drawable.toString(), "mipmap", context.packageName)
val d = context.resources.getDrawable(resId)
mEntryDrawables.add(d)
}
widgetLayoutResource = R.layout.view_icon
}
protected fun createListAdapter(): ListAdapter {
val selectedValue = value
val selectedIndex = findIndexOfValue(selectedValue)
return AppArrayAdapter(context, R.layout.item_icon_preference, entries, mEntryDrawables, selectedIndex)
}
override fun onBindView(view: View) {
super.onBindView(view)
val selectedValue = value
val selectedIndex = findIndexOfValue(selectedValue)
val drawable = mEntryDrawables[selectedIndex]
(view.findViewById<View>(R.id.preview) as ImageView).setImageDrawable(drawable)
}
override fun onPrepareDialogBuilder(builder: Builder) {
builder.setAdapter(createListAdapter(), this)
super.onPrepareDialogBuilder(builder)
}
inner class AppArrayAdapter(
context: Context, textViewResourceId: Int,
objects: Array<CharSequence>, imageDrawables: List<Drawable>,
selectedIndex: Int
) : ArrayAdapter<CharSequence>(context, textViewResourceId, objects) {
private var mImageDrawables: List<Drawable>? = null
private var mSelectedIndex = 0
init {
mSelectedIndex = selectedIndex
mImageDrawables = imageDrawables
}
@SuppressLint("ViewHolder")
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val inflater = (context as Activity).layoutInflater
val view = inflater.inflate(R.layout.item_icon_preference, parent, false)
val textView = view.findViewById<View>(R.id.label) as CheckedTextView
textView.text = getItem(position)
textView.isChecked = position == mSelectedIndex
val imageView = view.findViewById<View>(R.id.icon) as ImageView
imageView.setImageDrawable(mImageDrawables!![position])
return view
}
}
}

@ -0,0 +1,33 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.lib.theme.ColorUtil
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.dp
class ATEAccentBgTextView : AppCompatTextView {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
background = Selector.shapeBuild()
.setCornerRadius(3.dp)
.setDefaultBgColor(ThemeStore.accentColor(context))
.setPressedBgColor(ColorUtil.darkenColor(ThemeStore.accentColor(context)))
.create()
setTextColor(Color.WHITE)
}
}

@ -0,0 +1,39 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.R
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.dp
class ATEAccentStrokeTextView : AppCompatTextView {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
background = Selector.shapeBuild()
.setCornerRadius(3.dp)
.setStrokeWidth(1.dp)
.setDisabledStrokeColor(context.resources.getColor(R.color.md_grey_500))
.setDefaultStrokeColor(ThemeStore.accentColor(context))
.setPressedBgColor(context.resources.getColor(R.color.transparent30))
.create()
setTextColor(
Selector.colorBuild()
.setDefaultColor(ThemeStore.accentColor(context))
.setDisabledColor(context.resources.getColor(R.color.md_grey_500))
.create()
)
}
}

@ -0,0 +1,33 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
class ATEAutoCompleteTextView : AppCompatAutoCompleteTextView {
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context)
}
private fun init(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
backgroundTintList = Selector.colorBuild()
.setFocusedColor(ThemeStore.accentColor(context))
.setDefaultColor(ThemeStore.textColorPrimary(context))
.create()
}
}
}

@ -0,0 +1,29 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatCheckBox
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATECheckBox : AppCompatCheckBox {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
}

@ -0,0 +1,29 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatEditText
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATEEditText : AppCompatEditText {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
}

@ -0,0 +1,28 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATEPrimaryTextView : AppCompatTextView {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
setTextColor(ThemeStore.textColorPrimary(context))
}
}

@ -0,0 +1,41 @@
package io.legado.app.lib.theme.view
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.widget.ProgressBar
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATEProgressBar : ProgressBar {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
}

@ -0,0 +1,29 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatRadioButton
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATERadioButton : AppCompatRadioButton {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
}

@ -0,0 +1,37 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatRadioButton
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.dp
/**
* @author Aidan Follestad (afollestad)
*/
class ATERadioNoButton : AppCompatRadioButton {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
background = Selector.shapeBuild()
.setCornerRadius(3.dp)
.setStrokeWidth(3.dp)
.setCheckedBgColor(ThemeStore.accentColor(context))
.setCheckedStrokeColor(ThemeStore.accentColor(context))
.setDefaultStrokeColor(Color.WHITE)
.create()
}
}

@ -0,0 +1,28 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATESecondaryTextView : AppCompatTextView {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
setTextColor(ThemeStore.textColorSecondary(context))
}
}

@ -0,0 +1,29 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatSeekBar
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATESeekBar : AppCompatSeekBar {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
}

@ -0,0 +1,34 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Switch
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATEStockSwitch : Switch {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
override fun isShown(): Boolean {
return parent != null && visibility == View.VISIBLE
}
}

@ -0,0 +1,41 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.R
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.dp
class ATEStrokeTextView : AppCompatTextView {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
background = Selector.shapeBuild()
.setCornerRadius(1.dp)
.setStrokeWidth(1.dp)
.setDisabledStrokeColor(context.resources.getColor(R.color.md_grey_500))
.setDefaultStrokeColor(ThemeStore.textColorSecondary(context))
.setSelectedStrokeColor(ThemeStore.accentColor(context))
.setPressedBgColor(context.resources.getColor(R.color.transparent30))
.create()
setTextColor(
Selector.colorBuild()
.setDefaultColor(ThemeStore.textColorSecondary(context))
.setSelectedColor(ThemeStore.accentColor(context))
.setDisabledColor(context.resources.getColor(R.color.md_grey_500))
.create()
)
}
}

@ -0,0 +1,34 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Switch
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
/**
* @author Aidan Follestad (afollestad)
*/
class ATESwitch : Switch {
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
ATH.setTint(this, ThemeStore.accentColor(context))
}
override fun isShown(): Boolean {
return parent != null && visibility == View.VISIBLE
}
}

@ -0,0 +1,31 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputLayout
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
class ATETextInputLayout : TextInputLayout {
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context)
}
private fun init(context: Context) {
defaultHintTextColor = Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
}
}

@ -1,13 +1,19 @@
package io.legado.app.ui.about package io.legado.app.ui.about
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.AndroidViewModel
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.utils.getViewModel
class AboutActivity : AppCompatActivity() { class AboutActivity : BaseActivity<AndroidViewModel>() {
override val viewModel: AndroidViewModel
get() = getViewModel(AndroidViewModel::class.java)
override val layoutID: Int
get() = R.layout.activity_about
override fun onCreate(savedInstanceState: Bundle?) { override fun onViewModelCreated(viewModel: AndroidViewModel, savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onViewModelCreated(viewModel, savedInstanceState)
setContentView(R.layout.activity_about)
} }
} }

@ -21,7 +21,7 @@ import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.* import kotlinx.android.synthetic.main.app_bar_main.*
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
class MainActivity : BaseActivity<MainDataBinding, MainViewModel>(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : BaseActivity<MainViewModel>(), NavigationView.OnNavigationItemSelectedListener {
override val viewModel: MainViewModel override val viewModel: MainViewModel
get() = getViewModel(MainViewModel::class.java) get() = getViewModel(MainViewModel::class.java)

@ -2,34 +2,37 @@ package io.legado.app.ui.replacerule
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.constant.AppConst.APP_TAG
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_replace_rule.* import kotlinx.android.synthetic.main.activity_replace_rule.*
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ItemTouchHelper
import io.legado.app.constant.AppConst.APP_TAG
import kotlinx.android.synthetic.main.item_relace_rule.*
class ReplaceRuleActivity : AppCompatActivity() { class ReplaceRuleActivity : BaseActivity<ReplaceRuleViewModel>() {
override val viewModel: ReplaceRuleViewModel
get() = getViewModel(ReplaceRuleViewModel::class.java)
override val layoutID: Int
get() = R.layout.activity_replace_rule
private lateinit var adapter: ReplaceRuleAdapter private lateinit var adapter: ReplaceRuleAdapter
private var rulesLiveData: LiveData<PagedList<ReplaceRule>>? = null private var rulesLiveData: LiveData<PagedList<ReplaceRule>>? = null
private var allEnabled = false private var allEnabled = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onViewModelCreated(viewModel: ReplaceRuleViewModel, savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onViewModelCreated(viewModel, savedInstanceState)
setContentView(R.layout.activity_replace_rule)
initRecyclerView() initRecyclerView()
initDataObservers() initDataObservers()
initSwipeToDelete() initSwipeToDelete()

@ -0,0 +1,11 @@
package io.legado.app.ui.replacerule;
import android.app.Application;
import io.legado.app.base.BaseViewModel;
import org.jetbrains.annotations.NotNull;
public class ReplaceRuleViewModel extends BaseViewModel {
public ReplaceRuleViewModel(@NotNull Application application) {
super(application);
}
}

@ -3,11 +3,10 @@ package io.legado.app.ui.search
import android.os.Bundle import android.os.Bundle
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.data.entities.rule.Rule
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() { class SearchActivity : BaseActivity<SearchViewModel>() {
override val viewModel: SearchViewModel override val viewModel: SearchViewModel
get() = getViewModel(SearchViewModel::class.java) get() = getViewModel(SearchViewModel::class.java)

@ -0,0 +1,7 @@
package io.legado.app.utils
import androidx.appcompat.app.AlertDialog
import io.legado.app.lib.theme.ATH
val AlertDialog.upTint: AlertDialog
get() = ATH.setAlertDialogTint(this)

@ -0,0 +1,16 @@
package io.legado.app.utils
import android.content.res.Resources
val Float.dp: Float // [xxhdpi](360 -> 1080)
get() = android.util.TypedValue.applyDimension(
android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
)
val Float.sp: Float // [xxhdpi](360 -> 1080)
get() = android.util.TypedValue.applyDimension(
android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics
)

@ -0,0 +1,13 @@
package io.legado.app.utils
import android.content.res.Resources
val Int.dp: Int
get() = android.util.TypedValue.applyDimension(
android.util.TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()
val Int.sp: Int
get() = android.util.TypedValue.applyDimension(
android.util.TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/btn_bg_press_2">
<item android:id="@android:id/mask" android:drawable="@color/btn_bg_press_2"/>
</ripple>

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/btn_bg_press"/>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="1dp"
android:color="@color/bg_divider_line"
android:dashWidth="3dp"
android:dashGap="3dp"/>
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:width="1dp"
android:color="@color/btn_bg_press"/>
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/transparent" android:state_pressed="false"/>
<item android:drawable="@color/btn_bg_press" android:state_pressed="true"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
tools:ignore="PrivateResource">
<item android:drawable="@drawable/abc_textfield_search_activated_mtrl_alpha" android:state_enabled="true"
android:state_focused="true"/>
<item android:drawable="@drawable/abc_textfield_search_activated_mtrl_alpha" android:state_activated="true"
android:state_enabled="true"/>
<item android:drawable="@drawable/abc_textfield_search_default_mtrl_alpha" android:state_enabled="true"/>
<item android:drawable="@drawable/abc_textfield_search_default_mtrl_alpha"/>
</selector>

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2016 L4 Digital LLC. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle">
<tools:solid android:color="#777777"/>
<corners
android:topLeftRadius="@dimen/fastscroll_bubble_radius"
android:topRightRadius="@dimen/fastscroll_bubble_radius"
android:bottomLeftRadius="@dimen/fastscroll_bubble_radius"
android:bottomRightRadius="0dp"/>
<size
android:height="@dimen/fastscroll_bubble_size"
android:width="@dimen/fastscroll_bubble_size"/>
<padding
android:left="@dimen/fastscroll_bubble_padding"
android:right="@dimen/fastscroll_bubble_padding"/>
</shape>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2016 L4 Digital LLC. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle">
<tools:solid android:color="#555555"/>
<corners android:radius="@dimen/fastscroll_handle_radius"/>
<size
android:height="@dimen/fastscroll_handle_height"
android:width="@dimen/fastscroll_handle_width"/>
</shape>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2016 L4 Digital LLC. All rights reserved.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle">
<tools:solid android:color="#CCCCCC"/>
<size android:width="@dimen/fastscroll_track_width"/>
</shape>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M7.699,18.758 L7.699,13.219 L4,9.742 L20,5.242 L13.332,18.574 L10.785,16.125 L7.699,18.758 Z M9.455,13.035 L12.888,16.267 L16.987,8.288 L9.455,13.035 Z M9.035,14.477 L9.035,15.887 L9.81,15.209 L9.035,14.477 Z M6.655,10.398 L8.449,12.086 L14.544,8.248 L6.655,10.398 Z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M12.822,20 L11.177,20 L11.177,12.821 L4,12.821 L4,11.178 L11.178,11.178 L11.178,4 L12.823,4 L12.823,11.179 L20,11.179 L20,12.822 L12.822,12.822 L12.822,20 Z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M5.689,20 C5.164,20,4.74,19.844,4.432,19.537 C3.242,18.345,4.772,15.546,5.863,13.892 C5.683,13.308,5.595,12.688,5.595,12 C5.595,8.469,8.467,5.596,11.998,5.596 C12.684,5.596,13.308,5.685,13.898,5.868 C15.013,5.131,16.948,4,18.339,4 C18.84,4,19.242,4.146,19.531,4.435 C20.308,5.212,20.118,6.648,18.965,8.697 C18.724,9.127,18.433,9.588,18.099,10.076 C18.296,10.703,18.396,11.345,18.396,11.986 C18.396,15.524,15.526,18.403,11.998,18.403 C11.349,18.403,10.7,18.304,10.067,18.103 C8.774,18.991,7.013,20,5.689,20 Z M6.51,15.317 C5.331,17.292,5.196,18.449,5.357,18.611 C5.418,18.673,5.543,18.706,5.709,18.706 C6.337,18.706,7.422,18.249,8.669,17.469 C7.791,16.935,7.046,16.193,6.51,15.317 Z M11.473,17.068 C11.651,17.087,11.826,17.096,11.998,17.096 C14.806,17.096,17.089,14.803,17.089,11.985 C17.089,11.82,17.081,11.651,17.063,11.482 C16.282,12.473,15.398,13.461,14.428,14.43 C13.48,15.38,12.47,16.28,11.473,17.068 Z M11.998,6.901 C9.188,6.901,6.902,9.19,6.902,12 C6.902,14.017,8.066,15.818,9.885,16.641 C11.084,15.764,12.301,14.711,13.504,13.508 C14.686,12.326,15.765,11.076,16.635,9.883 C15.811,8.068,14.011,6.901,11.998,6.901 Z M15.317,6.516 C16.19,7.051,16.929,7.791,17.461,8.666 C17.598,8.448,17.72,8.247,17.827,8.055 C18.848,6.24,18.729,5.482,18.607,5.36 C18.605,5.357,18.549,5.304,18.335,5.304 C18.084,5.304,17.163,5.396,15.317,6.516 Z"/>
</vector>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M12.167,4h-2.025c-0.604,0-1.08,0.529-1.08,1.206v13.588c0,0.677,0.476,1.206,1.08,1.206h2.025 c0.605,0,1.08-0.529,1.08-1.206V5.206C13.247,4.529,12.772,4,12.167,4z M11.792,5.454v9.531h-1.273V5.454H11.792z M10.519,18.546 v-2.106h1.273v2.106H10.519z"/>
<path
android:fillColor="#595757"
android:pathData="M7.104,6.693H5.079c-0.596,0-1.078,0.501-1.078,1.117v11.072C4.001,19.499,4.483,20,5.079,20h2.025 c0.596,0,1.081-0.501,1.081-1.117V7.811C8.186,7.195,7.7,6.693,7.104,6.693z M6.729,8.148v6.837H5.455V8.148H6.729z M5.455,18.546 v-2.106h1.274v2.106H5.455z"/>
<path
android:fillColor="#595757"
android:pathData="M17.976,7.433c-0.098-0.534-0.551-0.921-1.075-0.921l-2.183,0.383 c-0.584,0.108-0.969,0.689-0.858,1.294l2.006,10.89c0.099,0.534,0.55,0.92,1.074,0.92l2.183-0.383 c0.585-0.108,0.969-0.689,0.859-1.294L17.976,7.433z M15.354,8.256l1.25-0.234l1.289,7.004l-1.256,0.209L15.354,8.256z M17.235,18.484L16.9,16.667l1.257-0.209l0.33,1.797L17.235,18.484z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,10l5,5 5,-5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,14l5,-5 5,5z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,3c-4.963,0 -9,4.037 -9,9s4.037,9 9,9s9,-4.037 9,-9S16.963,3 12,3zM12,13.826c-2.277,0 -4.371,1.177 -5.542,3.092C5.245,15.554 4.581,13.82 4.581,12c0,-4.091 3.328,-7.419 7.419,-7.419S19.419,7.909 19.419,12c0,1.82 -0.664,3.554 -1.877,4.918C16.371,15.003 14.277,13.826 12,13.826zM12,15.407c1.811,0 3.489,1.013 4.33,2.598c-1.27,0.926 -2.762,1.414 -4.33,1.414c-1.568,0 -3.062,-0.488 -4.332,-1.414C8.511,16.42 10.189,15.407 12,15.407z"
android:fillColor="#4A4B4A"/>
<path
android:pathData="M12,6.014c-2.178,0 -3.951,1.746 -3.951,3.893c0,2.149 1.772,3.899 3.951,3.899s3.951,-1.75 3.951,-3.899C15.951,7.76 14.178,6.014 12,6.014zM12,12.225c-1.307,0 -2.37,-1.041 -2.37,-2.318c0,-1.274 1.063,-2.312 2.37,-2.312s2.37,1.037 2.37,2.312C14.37,11.184 13.307,12.225 12,12.225z"
android:fillColor="#4A4B4A"/>
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M19.109,6.212h-2.16l-0.002-0.817c0-0.366-0.164-0.66-0.44-0.788 c-0.133-0.062-0.278-0.091-0.445-0.091c-0.295,0-0.595,0.092-0.885,0.181l-5.121,1.516H4.891c-0.569,0-1.016,0.407-1.016,0.927 v9.907c0,0.52,0.446,0.926,1.016,0.926h5.283c0.693,0.19,4.949,1.359,5.308,1.463c0.115,0.033,0.229,0.05,0.339,0.05 c0.529,0,1.083-0.422,1.098-0.84l0.002-0.673h2.189c0.569,0,1.016-0.406,1.016-0.926V7.139C20.125,6.619,19.679,6.212,19.109,6.212 z M18.687,7.65v8.883h-1.763l0.023-8.883H18.687z M15.516,17.95c-0.723-0.204-2.282-0.65-3.459-0.988 c-0.794-0.228-1.414-0.405-1.485-0.425l-5.258-0.004V7.65h5.225l4.972-1.524L15.516,17.95z"/>
<path
android:fillColor="#595757"
android:pathData="M 9.544 8.915 L 6.366 12.092 L 9.544 15.269 L 10.562 14.252 L 9.12 12.811 L 14.217 12.811 L 14.217 11.372 L 9.12 11.372 L 10.562 9.932 Z"/>
</vector>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M19.109,6.212h-2.16l-0.002-0.817c0-0.366-0.164-0.66-0.44-0.788 c-0.133-0.062-0.278-0.091-0.445-0.091c-0.295,0-0.595,0.092-0.885,0.181l-5.121,1.516H4.891c-0.569,0-1.016,0.407-1.016,0.927 v9.907c0,0.52,0.446,0.926,1.016,0.926h5.283c0.693,0.19,4.949,1.359,5.308,1.463c0.115,0.033,0.229,0.05,0.339,0.05 c0.529,0,1.083-0.422,1.098-0.84l0.002-0.673h2.189c0.569,0,1.016-0.406,1.016-0.926V7.139C20.125,6.619,19.679,6.212,19.109,6.212 z M12.057,16.962c-0.794-0.228-1.414-0.405-1.485-0.425l-5.258-0.004V7.65h5.225l4.972-1.524l0.006,11.824 C14.793,17.746,13.233,17.3,12.057,16.962z M18.687,16.533h-1.763l0.023-8.883h1.739V16.533z"/>
<path
android:fillColor="#595757"
android:pathData="M 8.208 9.5 H 9.646 V 14.5 H 8.208 V 9.5 Z"/>
<path
android:fillColor="#595757"
android:pathData="M 11.396 9.5 H 12.834 V 14.5 H 11.396 V 9.5 Z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="64.063"
android:viewportHeight="64">
<path
android:fillColor="#000000"
android:pathData="M54.054,49.348H24.72c-3.033,0-5.5-2.466-5.5-5.499V18.94l5.12,5.121c0.358,0.358,0.828,0.537,1.296,0.537 c0.469,0,0.938-0.179,1.297-0.537c0.715-0.716,0.715-1.877,0-2.592l-8.25-8.25c-0.716-0.716-1.876-0.716-2.592,0l-8.25,8.25 c-0.715,0.715-0.715,1.876,0,2.592c0.717,0.716,1.877,0.716,2.593,0l5.12-5.121v24.908c0,5.054,4.113,9.166,9.167,9.166h29.333 c1.014,0,1.833-0.819,1.833-1.833S55.067,49.348,54.054,49.348L54.054,49.348z"/>
</vector>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M15.923,11.848 C13.675,11.848,11.846,13.678,11.846,15.924 C11.846,18.172,13.675,20,15.923,20 S20,18.172,20,15.924 C20,13.678,18.171,11.848,15.923,11.848 Z M15.923,18.713 C14.385,18.713,13.133,17.461,13.133,15.924 C13.133,14.385,14.385,13.135,15.923,13.135 C17.461,13.135,18.713,14.385,18.713,15.924 C18.713,17.461,17.461,18.713,15.923,18.713 Z"/>
<path
android:fillColor="#595757"
android:pathData="M16.567,13.961 L15.279,13.961 L15.279,16.568 L17.886,16.568 L17.886,15.279 L16.567,15.279 Z"/>
<path
android:fillColor="#595757"
android:pathData="M5.287,5.623 C5.287,5.439,5.439,5.289,5.625,5.289 L16.412,5.289 C16.599,5.289,16.751,5.439,16.751,5.623 L16.751,11.019 L18.038,11.019 L18.038,5.623 C18.038,4.729,17.309,4,16.412,4 L5.625,4 C4.73,4,4,4.729,4,5.623 L4,18.375 C4,19.273,4.73,20,5.625,20 L11.018,20 L11.018,18.713 L5.625,18.713 C5.439,18.713,5.287,18.561,5.287,18.375 L5.287,18.039 L11.018,18.039 L11.018,16.752 L5.287,16.752 L5.287,5.623 Z"/>
<path
android:fillColor="#595757"
android:pathData="M5.96,6.115 L7.247,6.115 L7.247,7.095 L5.96,7.095 L5.96,6.115 Z"/>
<path
android:fillColor="#595757"
android:pathData="M14.788,6.115 L16.075,6.115 L16.075,7.095 L14.788,7.095 L14.788,6.115 Z"/>
<path
android:fillColor="#595757"
android:pathData="M5.96,14.943 L7.247,14.943 L7.247,15.923 L5.96,15.923 L5.96,14.943 Z"/>
<path
android:fillColor="#595757"
android:pathData="M11.474,11.475 L13.387,9.561 C13.65,9.987,13.809,10.483,13.809,11.02 L15.096,11.02 C15.096,10.127,14.799,9.308,14.311,8.636 L14.908,8.039 L13.997,7.131 L13.4,7.727 C12.728,7.24,11.909,6.944,11.018,6.944 C8.77,6.944,6.941,8.772,6.941,11.02 S8.77,15.096,11.018,15.096 L11.018,13.807 C9.48,13.807,8.228,12.557,8.228,11.02 C8.228,9.479,9.479,8.229,11.018,8.229 C11.554,8.229,12.05,8.388,12.476,8.651 L10.563,10.563 L11.474,11.475 Z"/>
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="680.31"
android:viewportWidth="680.31">
<path
android:fillColor="#2C2C2C"
android:pathData="M339.865,79.038c-147.014,0-266.518,119.198-266.518,266.519 c0,147.011,119.198,266.519,266.518,266.519c147.013,0,266.52-119.201,266.52-266.519 C606.078,198.542,486.878,79.038,339.865,79.038L339.865,79.038z M339.865,587.929c-133.871,0-242.373-108.505-242.373-242.373 c0-133.872,108.502-242.066,242.373-242.066c133.868,0,242.372,108.5,242.372,242.373 C582.237,479.731,473.432,587.929,339.865,587.929L339.865,587.929z M339.865,587.929"/>
<path
android:fillColor="#2C2C2C"
android:pathData="M305.329,491.346c20.782,0,28.118,33.619,28.118,33.619h12.836c0,0,7.337-33.619,28.12-33.619h133.869 c7.95,0,14.673-6.725,14.673-14.671V242.25c0-33.316-33.316-33.316-33.316-33.316H361.261c-10.697,0-21.396,13.449-21.396,20.479 c0-7.03-10.391-20.479-21.088-20.479H190.103c0,0-33.317,0-33.317,33.316v234.426c0,8.253,6.418,14.671,14.67,14.671H305.329z M348.117,236.75c0-6.115,4.89-11.31,11.005-11.31h139.065c3.973,0,7.336,3.361,7.336,7.64l0.607,235.647 c0,4.279-3.36,7.644-7.333,7.644H372.873c-14.06,0-24.756,15.589-24.756,15.589V236.75z M173.902,233.08 c0-4.279,3.364-7.64,7.336-7.64h139.066c6.112,0,11.003,4.891,11.003,11.31v254.903c0,0-11.003-15.589-24.759-15.589H180.627 c-3.974,0-7.337-3.36-7.337-7.639L173.902,233.08z M173.902,233.08"/>
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-0.756,0a0.756,0.756 0,1 1,1.512 0a0.756,0.756 0,1 1,-1.512 0"
android:fillColor="#4A4B4A"/>
<path
android:pathData="M7.038,16.963l6.94,-2.985l2.984,-6.939l-6.938,2.985L7.038,16.963zM14.195,9.804l-1.321,3.071l-3.069,1.322l1.321,-3.07L14.195,9.804z"
android:fillColor="#4A4B4A"/>
<path
android:pathData="M12,3c-4.963,0 -9,4.037 -9,9s4.037,9 9,9s9,-4.037 9,-9S16.963,3 12,3zM12,19.419c-4.091,0 -7.419,-3.328 -7.419,-7.419S7.909,4.581 12,4.581S19.419,7.909 19.419,12S16.091,19.419 12,19.419z"
android:fillColor="#4A4B4A"/>
</vector>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M20,7.046 L4,7.046 L4,5.522 L20,5.522 L20,7.046 Z"/>
<path
android:fillColor="#595757"
android:pathData="M20,10.855 L4,10.855 L4,9.334 L20,9.334 L20,10.855 Z"/>
<path
android:fillColor="#595757"
android:pathData="M20,14.666 L4,14.666 L4,13.145 L20,13.145 L20,14.666 Z"/>
<path
android:fillColor="#595757"
android:pathData="M20,18.477 L4,18.477 L4,16.954 L20,16.954 L20,18.477 Z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M5.938,4v16h1.414l4.648-2.789L16.649,20h1.411V4H5.938z M16.606,18.278l-4.605-2.763l-4.609,2.764 V5.454h9.215V18.278z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M12,20 C7.59,20,4,16.412,4,12 S7.59,4,12,4 L12.848,4 L12.698,4.835 C12.693,4.862,12.221,7.656,13.793,9.529 C14.782,10.711,16.402,11.306,18.606,11.307 L18.606,11.307 C18.816,11.307,19.032,11.304,19.255,11.29 L20,11.256 L20,12 C20,16.412,16.411,20,12,20 Z M11.215,5.463 C7.955,5.854,5.418,8.635,5.418,12 C5.418,15.631,8.371,18.583,12,18.583 C15.384,18.583,18.18,16.015,18.543,12.727 C15.925,12.713,13.958,11.943,12.698,10.43 C11.305,8.762,11.165,6.641,11.215,5.463 Z"/>
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M12,4c-4.412,0-8,3.588-8,8s3.588,8,8,8c4.41,0,8-3.588,8-8S16.41,4,12,4z M12,18.545 c-3.608,0-6.546-2.936-6.546-6.545S8.392,5.455,12,5.455c3.607,0,6.545,2.936,6.545,6.545S15.607,18.545,12,18.545z"/>
<path
android:fillColor="#595757"
android:pathData="M 13.426 9.546 L 12.002 10.972 L 10.576 9.546 L 9.547 10.575 L 10.973 12 L 9.547 13.425 L 10.576 14.454 L 12.002 13.028 L 13.426 14.454 L 14.454 13.425 L 13.029 12 L 14.454 10.575 Z"/>
</vector>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M20,7.046H4V5.522h16V7.046z"/>
<path
android:fillColor="#595757"
android:pathData="M20,10.855H4V9.334h16V10.855z"/>
<path
android:fillColor="#595757"
android:pathData="M20,14.666H4v-1.521h16V14.666z"/>
<path
android:fillColor="#595757"
android:pathData="M20,18.477H4v-1.523h16V18.477z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M20,6.182h-1.454v1.249C17.067,5.315,14.646,4,12,4c-4.412,0-8,3.589-8,8s3.588,8,8,8 c3.789,0,7.086-2.691,7.84-6.401l-1.426-0.289C17.798,16.344,15.1,18.546,12,18.546c-3.608,0-6.546-2.937-6.546-6.546 S8.392,5.454,12,5.454c2.399,0,4.576,1.32,5.721,3.395h-1.842v1.454H20V6.182z"/>
<path
android:fillColor="#595757"
android:pathData="M11.273,8.363c-1.604,0-2.909,1.305-2.909,2.91c0,1.604,1.305,2.909,2.909,2.909 c0.537,0,1.035-0.157,1.468-0.412l2.14,2.139l1.027-1.029l-2.139-2.139c0.256-0.432,0.412-0.93,0.412-1.468 C14.182,9.668,12.877,8.363,11.273,8.363z M9.818,11.273c0-0.802,0.652-1.455,1.455-1.455c0.801,0,1.453,0.653,1.453,1.455 c0,0.801-0.652,1.454-1.453,1.454C10.471,12.727,9.818,12.074,9.818,11.273z"/>
</vector>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M 18.303 8.091 L 14.939 8.091 L 14.939 4.727 L 15.848 4.727 L 15.848 7.182 L 18.303 7.182 Z"/>
<path
android:fillColor="#595757"
android:pathData="M19.03,18.061H6.91V4h8.785l3.335,3.335V18.061z M8.363,16.606h9.213V7.938l-2.483-2.484H8.363 V16.606z"/>
<path
android:fillColor="#595757"
android:pathData="M 17.09 20 L 4.969 20 L 4.969 5.939 L 7.636 5.939 L 7.636 7.394 L 6.424 7.394 L 6.424 18.546 L 15.636 18.546 L 15.636 17.333 L 17.09 17.333 Z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save