>(paths: Vec) -> String {
9 | match paths.len() {
10 | 0 => String::default(),
11 | _ => {
12 | let mut path: PathBuf = PathBuf::new();
13 | for x in paths {
14 | path = path.join(x);
15 | }
16 | return path.to_str().unwrap().to_string();
17 | }
18 | }
19 | }
20 |
21 | pub(crate) fn create_dir_if_not_exists(path: &str) {
22 | if !Path::new(path).exists() {
23 | std::fs::create_dir_all(path).unwrap();
24 | }
25 | }
26 |
27 | lazy_static! {
28 | static ref HASH_LOCK: Vec> = {
29 | let mut mutex_vec: Vec> = vec![];
30 | for _ in 0..64 {
31 | mutex_vec.push(Mutex::<()>::new(()));
32 | }
33 | mutex_vec
34 | };
35 | }
36 |
37 | pub(crate) async fn hash_lock(url: &String) -> MutexGuard<'static, ()> {
38 | let mut s = DefaultHasher::new();
39 | s.write(url.as_bytes());
40 | HASH_LOCK[s.finish() as usize % HASH_LOCK.len()]
41 | .lock()
42 | .await
43 | }
44 |
45 | pub(crate) fn allowed_file_name(title: &str) -> String {
46 | title
47 | .replace("#", "_")
48 | .replace("'", "_")
49 | .replace("/", "_")
50 | .replace("\\", "_")
51 | .replace(":", "_")
52 | .replace("*", "_")
53 | .replace("?", "_")
54 | .replace("\"", "_")
55 | .replace(">", "_")
56 | .replace("<", "_")
57 | .replace("|", "_")
58 | .replace("&", "_")
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/native/src/database/properties/property.rs:
--------------------------------------------------------------------------------
1 | use crate::database::properties::PROPERTIES_DATABASE;
2 | use crate::database::{create_index, create_table_if_not_exists, index_exists};
3 | use sea_orm::entity::prelude::*;
4 | use sea_orm::IntoActiveModel;
5 | use sea_orm::Set;
6 | use std::ops::Deref;
7 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
8 | #[sea_orm(table_name = "property")]
9 | pub struct Model {
10 | #[sea_orm(primary_key, auto_increment = false)]
11 | pub k: String,
12 | pub v: String,
13 | }
14 |
15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
16 | pub enum Relation {}
17 |
18 | impl ActiveModelBehavior for ActiveModel {}
19 |
20 | pub(crate) async fn init() {
21 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await;
22 | create_table_if_not_exists(db.deref(), Entity).await;
23 | if !index_exists(db.deref(), "property", "property_idx_k").await {
24 | create_index(db.deref(), "property", vec!["k"], "property_idx_k").await;
25 | }
26 | }
27 |
28 | pub async fn save_property(k: String, v: String) -> anyhow::Result<()> {
29 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await;
30 | if let Some(in_db) = Entity::find_by_id(k.clone()).one(db.deref()).await? {
31 | let mut in_db = in_db.into_active_model();
32 | in_db.v = Set(v);
33 | in_db.update(db.deref()).await?;
34 | } else {
35 | Model { k, v }
36 | .into_active_model()
37 | .insert(db.deref())
38 | .await?;
39 | }
40 | Ok(())
41 | }
42 |
43 | pub async fn load_property(k: String) -> anyhow::Result {
44 | let in_db = Entity::find_by_id(k)
45 | .one(PROPERTIES_DATABASE.get().unwrap().lock().await.deref())
46 | .await?;
47 | Ok(if let Some(in_db) = in_db {
48 | in_db.v
49 | } else {
50 | "".to_owned()
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/entry/src/ohosTest/ets/test/Ability.test.ets:
--------------------------------------------------------------------------------
1 | import { hilog } from '@kit.PerformanceAnalysisKit';
2 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
3 |
4 | export default function abilityTest() {
5 | describe('ActsAbilityTest', () => {
6 | // Defines a test suite. Two parameters are supported: test suite name and test suite function.
7 | beforeAll(() => {
8 | // Presets an action, which is performed only once before all test cases of the test suite start.
9 | // This API supports only one parameter: preset action function.
10 | })
11 | beforeEach(() => {
12 | // Presets an action, which is performed before each unit test case starts.
13 | // The number of execution times is the same as the number of test cases defined by **it**.
14 | // This API supports only one parameter: preset action function.
15 | })
16 | afterEach(() => {
17 | // Presets a clear action, which is performed after each unit test case ends.
18 | // The number of execution times is the same as the number of test cases defined by **it**.
19 | // This API supports only one parameter: clear action function.
20 | })
21 | afterAll(() => {
22 | // Presets a clear action, which is performed after all test cases of the test suite end.
23 | // This API supports only one parameter: clear action function.
24 | })
25 | it('assertContain', 0, () => {
26 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
27 | hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
28 | let a = 'abc';
29 | let b = 'b';
30 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
31 | expect(a).assertContain(b);
32 | expect(a).assertEqual(a);
33 | })
34 | })
35 | }
--------------------------------------------------------------------------------
/entry/src/main/ets/pages/components/LoginStorage.ets:
--------------------------------------------------------------------------------
1 | import { Action, Reducer, StateStore, Store } from "@hadss/state_store";
2 | import { hilog } from "@kit.PerformanceAnalysisKit";
3 | import { initLoginState } from "native";
4 | import { UiLoginState } from "native";
5 | import { loadProperty } from "native";
6 | import { login } from "native";
7 |
8 |
9 | @ObservedV2
10 | export class LoginStoreModel {
11 | @Trace public loginInfo: UiLoginState = {
12 | state: 0,
13 | message: ""
14 | };
15 | }
16 |
17 | export class LoginStoreActions {
18 | static preLogin: Action = StateStore.createAction('preLogin');
19 | static login: Action = StateStore.createAction('login');
20 | }
21 |
22 | export const loginStoreReducer: Reducer = (state: LoginStoreModel, action: Action) => {
23 | hilog.info(0x0000, 'StateStore', 'actions: %{public}s', action.type);
24 | switch (action.type) {
25 | case LoginStoreActions.preLogin.type:
26 | return async () => {
27 | if (state.loginInfo.state == -1) {
28 | return
29 | }
30 | state.loginInfo = {
31 | state: -1,
32 | message: ""
33 | };
34 | state.loginInfo = await initLoginState();
35 | }
36 | case LoginStoreActions.login.type:
37 | return async () => {
38 | if (state.loginInfo.state == -1) {
39 | return
40 | }
41 | state.loginInfo = {
42 | state: -1,
43 | message: ""
44 | };
45 | let username = await loadProperty("username");
46 | let password = await loadProperty("password");
47 | state.loginInfo = await login(
48 | username,
49 | password,
50 | );
51 | };
52 | }
53 | return null;
54 | }
55 |
56 | export const LoginStore: Store =
57 | StateStore.createStore('LoginStore', new LoginStoreModel(), loginStoreReducer, []);
58 |
59 |
--------------------------------------------------------------------------------
/entry/src/main/ets/entryability/EntryAbility.ets:
--------------------------------------------------------------------------------
1 | import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
2 | import { hilog } from '@kit.PerformanceAnalysisKit';
3 | import { window } from '@kit.ArkUI';
4 | import { init, cleanCache } from 'native'
5 |
6 | const DOMAIN = 0x0000;
7 |
8 | export default class EntryAbility extends UIAbility {
9 | async init(windowStage: window.WindowStage) {
10 | try {
11 | await init(this.context.filesDir);
12 | await cleanCache(3600 * 24 * 7);
13 | } catch (e) {
14 | hilog.error(DOMAIN, 'init', `${e}`);
15 | }
16 | windowStage.loadContent('pages/Index', (err) => {
17 | if (err.code) {
18 | hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
19 | return;
20 | }
21 | hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
22 | });
23 | }
24 |
25 | onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
26 | this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
27 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
28 | }
29 |
30 | onDestroy(): void {
31 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
32 | }
33 |
34 | onWindowStageCreate(windowStage: window.WindowStage): void {
35 | // Main window is created, set main page for this ability
36 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
37 | this.init(windowStage);
38 | }
39 |
40 | onWindowStageDestroy(): void {
41 | // Main window is destroyed, release UI related resources
42 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
43 | }
44 |
45 | onForeground(): void {
46 | // Ability has brought to foreground
47 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
48 | }
49 |
50 | onBackground(): void {
51 | // Ability has back to background
52 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
53 | }
54 | }
--------------------------------------------------------------------------------
/entry/src/main/ets/pages/components/ComicCard.ets:
--------------------------------------------------------------------------------
1 | import { Author } from 'native'
2 | import { colors } from './Context'
3 | import { CachedImage } from './CachedImage'
4 | import { materialIconData, materialIconsFontFamily } from './MaterialIcons'
5 |
6 | export interface ComicCardData {
7 | name: string
8 | pathWord: string
9 | author: Array
10 | cover: string
11 | popular: number
12 | datetimeUpdated?: string
13 | }
14 |
15 | @Entry
16 | @Component
17 | export struct ComicCard {
18 | @Require @Prop comic: ComicCardData
19 |
20 | build() {
21 | Flex() {
22 | CachedImage({
23 | source: this.comic.cover,
24 | useful: 'COMIC_COVER',
25 | extendsFieldFirst: this.comic.pathWord,
26 | borderOptions: { radius: 3.5 },
27 | imageWidth: 328 / 4,
28 | imageHeight: 422 / 4,
29 | })
30 | .width(328 / 4)
31 | .height(422 / 4)
32 | .flexShrink(0)
33 | .flexGrow(0)
34 | Blank(10)
35 | Column() {
36 | Blank(10)
37 | Text(`${this.comic.name}\n`)
38 | .maxLines(2)
39 | .fontWeight(FontWeight.Bold)
40 | Blank(10)
41 | Text(this.comic.author?.map(a => a.name)?.join("、") ?? "")
42 | .fontSize(14)
43 | .fontColor(colors.authorColor)
44 | Blank(10)
45 | Flex() {
46 | Text(this.comic.datetimeUpdated)
47 | .flexGrow(0)
48 | .flexShrink(0)
49 | Blank(1)
50 | .flexGrow(1)
51 | .flexShrink(1)
52 | Text(materialIconData('local_fire_department'))
53 | .fontFamily(materialIconsFontFamily)
54 | .fontColor(colors.authorColor)
55 | .fontSize(16)
56 | Text(` ${this.comic.popular}`)
57 | .flexGrow(0)
58 | .flexShrink(0)
59 | .fontSize(14)
60 | }
61 | }
62 | .flexGrow(1)
63 | .alignItems(HorizontalAlign.Start)
64 | }
65 | .padding({
66 | top: 8,
67 | bottom: 8,
68 | left: 15,
69 | right: 15
70 | })
71 | .border({
72 | color: '#33666666',
73 | width: .4,
74 | })
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/native/src/copy_client/types.rs:
--------------------------------------------------------------------------------
1 | use std::backtrace::Backtrace;
2 | use std::fmt::{Display, Formatter};
3 |
4 | pub type Result = std::result::Result;
5 |
6 | #[derive(Debug)]
7 | pub struct Error {
8 | pub backtrace: Backtrace,
9 | pub info: ErrorInfo,
10 | }
11 |
12 | #[derive(Debug)]
13 | pub enum ErrorInfo {
14 | Network(reqwest::Error),
15 | Message(String),
16 | Convert(serde_json::Error),
17 | Other(Box),
18 | }
19 |
20 | impl std::error::Error for Error {}
21 |
22 | impl Display for Error {
23 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24 | let mut builder = f.debug_struct("copy_client::Error");
25 | match &self.info {
26 | ErrorInfo::Convert(err) => {
27 | builder.field("kind", &"Convert");
28 | builder.field("source", err);
29 | }
30 | ErrorInfo::Network(err) => {
31 | builder.field("kind", &"Network");
32 | builder.field("source", err);
33 | }
34 | ErrorInfo::Message(err) => {
35 | builder.field("kind", &"Message");
36 | builder.field("source", err);
37 | }
38 | ErrorInfo::Other(err) => {
39 | builder.field("kind", &"Other");
40 | builder.field("source", err);
41 | }
42 | }
43 | builder.finish()
44 | }
45 | }
46 |
47 | impl Error {
48 | pub(crate) fn message(content: impl Into) -> Self {
49 | Self {
50 | backtrace: Backtrace::capture(),
51 | info: ErrorInfo::Message(content.into()),
52 | }
53 | }
54 | }
55 |
56 | macro_rules! from_error {
57 | ($error_type:ty, $info_type:path) => {
58 | impl From<$error_type> for Error {
59 | fn from(value: $error_type) -> Self {
60 | Self {
61 | backtrace: Backtrace::capture(),
62 | info: $info_type(value),
63 | }
64 | }
65 | }
66 | };
67 | }
68 |
69 | from_error!(::reqwest::Error, ErrorInfo::Network);
70 | from_error!(::serde_json::Error, ErrorInfo::Convert);
71 |
--------------------------------------------------------------------------------
/entry/src/main/ets/pages/components/Rank.ets:
--------------------------------------------------------------------------------
1 | import { ComicListData, DataExplore } from "./ComicListData"
2 | import { recommends, rank as nativeRank } from 'native'
3 | import { ComicCardList } from "./ComicCardList"
4 |
5 | @Component
6 | @Entry
7 | export struct Rank {
8 | @State dataList: ComicListData = new RecommendsListData()
9 |
10 | onChange(idx: number) {
11 | switch (idx) {
12 | case 0:
13 | this.dataList = new RecommendsListData()
14 | break;
15 | case 1:
16 | this.dataList = new RankListData("day")
17 | break;
18 | case 2:
19 | this.dataList = new RankListData("week")
20 | break;
21 | case 3:
22 | this.dataList = new RankListData("month")
23 | break;
24 | case 4:
25 | this.dataList = new RankListData("total")
26 | break;
27 | }
28 | }
29 |
30 | build() {
31 | Column() {
32 | Tabs({}) {
33 | TabContent() {
34 |
35 | }.tabBar('荐')
36 |
37 | TabContent() {
38 |
39 | }.tabBar('天')
40 |
41 | TabContent() {
42 |
43 | }.tabBar('周')
44 |
45 | TabContent() {
46 |
47 | }.tabBar('月')
48 |
49 | TabContent() {
50 |
51 | }.tabBar('总')
52 | }
53 | .barHeight(65)
54 | .height(65)
55 | .onChange((a) => this.onChange(a))
56 |
57 | ComicCardList({ listData: this.dataList })
58 | }
59 | .width('100%').height('100%')
60 | }
61 | }
62 |
63 |
64 | class RecommendsListData extends ComicListData {
65 | constructor() {
66 | super((offset, limit) => recommends(
67 | offset,
68 | limit,
69 | ));
70 | }
71 | }
72 |
73 | class RankListData extends ComicListData {
74 | constructor(rank: string) {
75 | super((o, l) => {
76 | return this.rankLoad(o, l, rank)
77 | });
78 | }
79 |
80 | rankLoad(offset: number, limit: number, rank: string): Promise {
81 | return nativeRank(rank, offset, limit).then(rankResult => {
82 | const a: DataExplore = {
83 | offset: rankResult.offset,
84 | limit: rankResult.limit,
85 | list: rankResult.list.map(r => {
86 | return r.comic
87 | }),
88 | };
89 | return a;
90 | })
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/native/src/copy_client/tests.rs:
--------------------------------------------------------------------------------
1 | use super::client::Client;
2 | use anyhow::Result;
3 | use base64::Engine;
4 | use reqwest::Method;
5 | use serde_json::json;
6 |
7 | const API_URL: &str = "aHR0cHM6Ly9hcGkuY29weW1hbmdhLm5ldA==";
8 |
9 | fn api_url() -> String {
10 | String::from_utf8(base64::prelude::BASE64_STANDARD.decode(API_URL).unwrap()).unwrap()
11 | }
12 |
13 | fn client() -> Client {
14 | Client::new(reqwest::Client::builder().build().unwrap(), api_url())
15 | }
16 |
17 | #[tokio::test]
18 | async fn test_request() -> Result<()> {
19 | let value = client()
20 | .request(
21 | Method::GET,
22 | "/api/v3/comics",
23 | json!({
24 | "_update": true,
25 | "limit": 21,
26 | "offset": 42,
27 | "platform": 3,
28 | }),
29 | )
30 | .await?;
31 | println!("{}", serde_json::to_string(&value).unwrap());
32 | Ok(())
33 | }
34 |
35 | #[tokio::test]
36 | async fn test_comic() -> Result<()> {
37 | let value = client().comic("dokunidakareteoboreteitai").await?;
38 | println!("{}", serde_json::to_string(&value).unwrap());
39 | Ok(())
40 | }
41 |
42 | #[tokio::test]
43 | async fn test_chapters() -> Result<()> {
44 | let value = client()
45 | .comic_chapter("fxzhanshijiuliumei", "default", 100, 0)
46 | .await?;
47 | println!("{}", serde_json::to_string(&value).unwrap());
48 | Ok(())
49 | }
50 |
51 | #[tokio::test]
52 | async fn test_recommends() -> Result<()> {
53 | let value = client().recommends(0, 21).await?;
54 | println!("{}", serde_json::to_string(&value).unwrap());
55 | Ok(())
56 | }
57 |
58 | #[tokio::test]
59 | async fn test_explore() -> Result<()> {
60 | let value = client()
61 | .explore(Some("-datetime_updated"), None, None, 0, 21)
62 | .await?;
63 | println!("{}", serde_json::to_string(&value).unwrap());
64 | Ok(())
65 | }
66 |
67 | #[tokio::test]
68 | async fn test_collect() -> Result<()> {
69 | let client = client();
70 | client.set_token("token").await;
71 | let value = client
72 | .collect("9581bff2-3892-11ec-8e8b-024352452ce0", true)
73 | .await?;
74 | println!("{}", serde_json::to_string(&value).unwrap());
75 | Ok(())
76 | }
77 |
--------------------------------------------------------------------------------
/entry/src/main/ets/pages/components/ComicCardList.ets:
--------------------------------------------------------------------------------
1 | import { IndicatorStatus, IndicatorWidget, LoadingMoreBase } from "@candies/loading_more_list"
2 | import { ComicCard, ComicCardData } from "./ComicCard"
3 | import { Error } from "./Error"
4 | import { Loading } from "./Loading"
5 | import { navStack } from "./Nav"
6 |
7 |
8 | @Component
9 | @Entry
10 | export struct ComicCardList {
11 | private scroller: Scroller = new ListScroller()
12 | @Require @Prop listData: LoadingMoreBase
13 |
14 | build() {
15 | if (this.listData) {
16 | this.buildList()
17 | }
18 | }
19 |
20 | @Builder
21 | buildList() {
22 | if (this.listData.indicatorStatus == IndicatorStatus.empty) {
23 | Text('空空如也')
24 | } else if (
25 | this.listData.indicatorStatus == IndicatorStatus.fullScreenError
26 | || (this.listData.totalCount() == 1 && `${this.listData.getData(0)}` == 'LoadingMoreErrorItem')
27 | ) {
28 | Error()
29 | .width('100%')
30 | .height('100%')
31 | .onClick(() => {
32 | this.listData.refresh(true);
33 | })
34 | } else if (this.listData.totalCount() == 1 && this.listData.isLoadingMoreItem(this.listData.getData(0))) {
35 | Loading()
36 | .width('100%')
37 | .height('100%')
38 | } else {
39 | this.foreachList()
40 | }
41 | }
42 |
43 | @Builder
44 | foreachList() {
45 | List({ scroller: this.scroller }) {
46 | ListItem() {
47 | Column() {
48 | }.height(10)
49 | }
50 |
51 | LazyForEach(this.listData, (item: ComicCardData, index) => {
52 | ListItem() {
53 | if (this.listData.isLoadingMoreItem(item)) {
54 | if (this.listData.getLoadingMoreItemStatus(item)) {
55 | if (IndicatorStatus.noMoreLoad == this.listData.getLoadingMoreItemStatus(item)) {
56 |
57 | } else {
58 | IndicatorWidget({
59 | indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
60 | sourceList: this.listData,
61 | })
62 | }
63 | }
64 | } else {
65 | ComicCard({ comic: item })
66 | .onClick(() => {
67 | navStack.pushPath(new NavPathInfo('pages/ComicInfo', item))
68 | })
69 | }
70 | }.width('100%')
71 | },
72 | )
73 | }
74 | .width('100%')
75 | .height('100%')
76 | .onReachEnd(() => {
77 | this.listData.loadMore();
78 | })
79 | }
80 | }
--------------------------------------------------------------------------------
/entry/src/main/ets/pages/components/CachedImage.ets:
--------------------------------------------------------------------------------
1 | import { cacheImage } from 'native'
2 | import { url } from '@kit.ArkTS';
3 | import { image } from '@kit.ImageKit';
4 | import { materialIconData, materialIconsFontFamily } from './MaterialIcons';
5 |
6 | @Entry
7 | @Component
8 | export struct CachedImage {
9 | @Require @Prop source: string
10 | @Prop useful: string
11 | @Prop extendsFieldFirst?: string
12 | @Prop extendsFieldSecond?: string
13 | @Prop extendsFieldThird?: string
14 | @Prop borderOptions?: BorderOptions
15 | @Prop imageWidth?: Length
16 | @Prop imageHeight?: Length
17 | @Prop ratio: number | null
18 | @Prop onSize: OnSize | null = null;
19 | @State state: number = 0
20 | @State pixelMap: image.PixelMap | null = null
21 | @State trueSize: image.Size | null = null
22 | @State absPath: string | null = null
23 |
24 | aboutToAppear(): void {
25 | this.init()
26 | }
27 |
28 | async init() {
29 | try {
30 | console.error(`load image : ${this.source}`)
31 | let ci = await cacheImage(
32 | this.cacheKey(this.source),
33 | this.source,
34 | this.useful ?? '',
35 | this.extendsFieldFirst ?? '',
36 | this.extendsFieldSecond ?? '',
37 | this.extendsFieldThird ?? '',
38 | )
39 | this.absPath = `file://${ci.absPath}`
40 | console.error(this.absPath)
41 | if (this.onSize != null) {
42 | this.onSize!.onSize({
43 | width: ci.imageWidth,
44 | height: ci.imageHeight,
45 | })
46 | }
47 | this.state = 1
48 | } catch (e) {
49 | this.state = 2
50 | console.error(`image error : ${e} `)
51 | }
52 | }
53 |
54 | cacheKey(source: string): string {
55 | let u = url.URL.parseURL(source)
56 | return u.pathname
57 | }
58 |
59 | build() {
60 | if (this.state == 1) {
61 | Image(this.absPath)
62 | .border(this.borderOptions)
63 | .width(this.imageWidth ?? '')
64 | .height(this.imageHeight ?? '')
65 | .aspectRatio(this.ratio)
66 | .objectFit(ImageFit.Cover)
67 | .renderFit(RenderFit.CENTER)
68 | } else {
69 | Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
70 | if (this.state == 0) {
71 | Text(materialIconData('download'))
72 | .fontFamily(materialIconsFontFamily)
73 | .fontSize(30)
74 | .fontColor('#666666')
75 | } else {
76 | Text(materialIconData('error'))
77 | .fontFamily(materialIconsFontFamily)
78 | .fontSize(30)
79 | .fontColor('#666666')
80 | }
81 | }
82 | .width(this.imageWidth)
83 | .height(this.imageHeight)
84 | .aspectRatio(this.ratio)
85 | }
86 | }
87 | }
88 |
89 | interface OnSize {
90 | onSize: ((size: image.Size) => void)
91 | }
92 |
--------------------------------------------------------------------------------
/native/src/database/download/download_comic_group.rs:
--------------------------------------------------------------------------------
1 | use crate::database::download::DOWNLOAD_DATABASE;
2 | use crate::database::{create_index, create_table_if_not_exists, index_exists};
3 | use sea_orm::entity::prelude::*;
4 | use sea_orm::sea_query::OnConflict;
5 | use sea_orm::{DeleteResult, Order, QueryOrder};
6 | use sea_orm::{IntoActiveModel};
7 | use serde_derive::{Deserialize, Serialize};
8 | use std::ops::Deref;
9 |
10 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
11 | #[sea_orm(table_name = "download_comic_group")]
12 | pub struct Model {
13 | #[sea_orm(primary_key, auto_increment = false)]
14 | pub comic_path_word: String,
15 | #[sea_orm(primary_key, auto_increment = false)]
16 | pub group_path_word: String,
17 | pub count: i64,
18 | pub name: String,
19 | //
20 | pub group_rank: i64,
21 | }
22 |
23 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
24 | pub enum Relation {}
25 |
26 | impl ActiveModelBehavior for ActiveModel {}
27 |
28 | pub(crate) async fn init() {
29 | let db = DOWNLOAD_DATABASE.get().unwrap().lock().await;
30 | create_table_if_not_exists(db.deref(), Entity).await;
31 | if !index_exists(
32 | db.deref(),
33 | "download_comic_group",
34 | "download_comic_group_idx_comic_path_word",
35 | )
36 | .await
37 | {
38 | create_index(
39 | db.deref(),
40 | "download_comic_group",
41 | vec!["comic_path_word"],
42 | "download_comic_group_idx_comic_path_word",
43 | )
44 | .await;
45 | }
46 | }
47 |
48 | pub(crate) async fn delete_by_comic_path_word(
49 | db: &impl ConnectionTrait,
50 | comic_path_word: &str,
51 | ) -> Result {
52 | Entity::delete_many()
53 | .filter(Column::ComicPathWord.eq(comic_path_word))
54 | .exec(db)
55 | .await
56 | }
57 |
58 | pub(crate) async fn insert_or_update_info(
59 | db: &impl ConnectionTrait,
60 | model: Model,
61 | ) -> Result<(), DbErr> {
62 | // https://www.sea-ql.org/SeaORM/docs/basic-crud/insert/
63 | // Performing an upsert statement without inserting or updating any of the row will result in a DbErr::RecordNotInserted error.
64 | // If you want RecordNotInserted to be an Ok instead of an error, call .do_nothing():
65 | Entity::insert(model.into_active_model())
66 | .on_conflict(
67 | OnConflict::columns(vec![Column::ComicPathWord, Column::GroupPathWord])
68 | .do_nothing()
69 | .to_owned(),
70 | )
71 | .exec(db)
72 | .await?;
73 | Ok(())
74 | }
75 |
76 | // find_by_comic_path_word order by rank
77 | pub(crate) async fn find_by_comic_path_word(comic_path_word: &str) -> anyhow::Result> {
78 | let db = DOWNLOAD_DATABASE.get().unwrap().lock().await;
79 | let result = Entity::find()
80 | .filter(Column::ComicPathWord.eq(comic_path_word))
81 | .order_by(Column::GroupRank, Order::Asc)
82 | .all(db.deref())
83 | .await?;
84 | Ok(result)
85 | }
86 |
--------------------------------------------------------------------------------
/native/src/database/cache/image_cache.rs:
--------------------------------------------------------------------------------
1 | use crate::database::cache::CACHE_DATABASE;
2 | use crate::database::{create_index, create_table_if_not_exists, index_exists};
3 | use sea_orm::entity::prelude::*;
4 | use sea_orm::sea_query::Expr;
5 | use sea_orm::EntityTrait;
6 | use sea_orm::IntoActiveModel;
7 | use sea_orm::QueryOrder;
8 | use sea_orm::QuerySelect;
9 | use std::ops::Deref;
10 |
11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
12 | #[sea_orm(table_name = "image_cache")]
13 | pub struct Model {
14 | #[sea_orm(primary_key, auto_increment = false)]
15 | pub cache_key: String,
16 | pub cache_time: i64,
17 | pub url: String,
18 | pub useful: String,
19 | pub extends_field_first: Option,
20 | pub extends_field_second: Option,
21 | pub extends_field_third: Option,
22 | pub local_path: String,
23 | pub image_format: String,
24 | pub image_width: u32,
25 | pub image_height: u32,
26 | }
27 |
28 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
29 | pub enum Relation {}
30 |
31 | impl ActiveModelBehavior for ActiveModel {}
32 |
33 | pub(crate) async fn init() {
34 | let gdb = CACHE_DATABASE.get().unwrap().lock().await;
35 | let db = gdb.deref();
36 | create_table_if_not_exists(db, Entity).await;
37 | if !index_exists(db, "image_cache", "image_cache_idx_cache_time").await {
38 | create_index(
39 | db,
40 | "image_cache",
41 | vec!["cache_time"],
42 | "image_cache_idx_cache_time",
43 | )
44 | .await;
45 | }
46 | }
47 |
48 | pub(crate) async fn load_image_by_cache_key(cache_key: &str) -> anyhow::Result