diff --git a/.github/workflows/prbuild.yml b/.github/workflows/prbuild.yml index 903f8b3d..e1208a0d 100644 --- a/.github/workflows/prbuild.yml +++ b/.github/workflows/prbuild.yml @@ -1,10 +1,12 @@ # This is a basic workflow to help you get started with Actions -name: prbuild +name: Dev Build CI on: pull_request: - branches: ["main"] + branches: ["main", "dev"] + push: + branches: ["dev"] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07a30d62..907b8bf7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,7 @@ # This is a basic workflow to help you get started with Actions -name: build +name: Release CI -# git push --tags 执行时候进行编译 on: push: tags: @@ -17,6 +16,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + ref: main - uses: actions/setup-node@v3 with: node-version: 16.x @@ -33,6 +33,8 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 + with: + ref: main - name: Flutter action uses: subosito/flutter-action@v2 with: @@ -81,6 +83,8 @@ jobs: # steps: # - uses: actions/checkout@v3 + # with: + # ref: main # - uses: subosito/flutter-action@v2 # with: # flutter-version: 3.10.3 @@ -117,6 +121,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + ref: main - uses: subosito/flutter-action@v2 with: flutter-version: 3.13.0 diff --git a/assets/i18n/be.json b/assets/i18n/be.json index 209965d3..dc59d407 100644 --- a/assets/i18n/be.json +++ b/assets/i18n/be.json @@ -64,9 +64,9 @@ "repo-url-subtitle": "Атрымаць URL рэпазітара для пашырэнняў", "tmdb-key": "Ключ API TMDB", "tmdb-key-subtitle": "Атрымаць ключ API для метададзеных TMDB", - "bt-server": "BT Сервер", - "bt-server-subtitle": "BT Сервер - неабходны кампанент для анлайн-прайгравання сядоў BT", - "bt-server-manager": "Мэнэджэр", + "bt-server": "Сервер BT", + "bt-server-subtitle": "Сэрвер BT гэта неабходны кампанент для онлайн-выканання торэнтаў", + "bt-server-manager": "кіраваць", "upgrade": "Абнаўленне ПЗ", "upgrade-subtitle": "версія: {version}", "upgrade-training": "Праверыць наяўнасць абнаўленняў", @@ -186,11 +186,14 @@ "languages": "Мовы" }, - "bt-server": { + "bt-server": { "not-installed": "BT-Сервер не ўсталяваны", "running": "BT-Сервер запушчаны", "stopped": "BT-Сервер спынены", "version": "Версія {version}", - "start": "Запуск" + "remote-version": "Далёкая Версія {version}", + "stop": "Спыніць", + "upgrade": "Абнавіць", + "start": "Запусціць" } } diff --git a/assets/i18n/en.json b/assets/i18n/en.json index f8326c19..6e273252 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -76,8 +76,8 @@ "tmdb-key": "TMDB API Key", "tmdb-key-subtitle": "Get API Key for TMDB metadata", "bt-server": "BT Server", - "bt-server-subtitle": "BT Server is a necessary component for online playback of BT seeds", - "bt-server-manager": "Manager", + "bt-server-subtitle": "BT Server is a necessary component for online playback of torrent", + "bt-server-manager": "Manage", "upgrade": "Update Software", "upgrade-subtitle": "version: {version}", "upgrade-training": "Check", @@ -89,6 +89,7 @@ "theme-system": "System", "theme-light": "Light", "theme-dark": "Dark", + "theme-black": "Black", "nsfw": "NSFW", "nsfw-subtitle": "Show NSFW content", "external-player": "Preferred video player", @@ -202,6 +203,9 @@ "running": "BT-Server is running", "stopped": "BT-Server is stopped", "version": "Version {version}", + "remote-version": "Remote Version {version}", + "stop": "Stop", + "upgrade": "Upgrade", "start": "Start" } } diff --git a/assets/i18n/es.json b/assets/i18n/es.json index ee88dce9..232370f3 100644 --- a/assets/i18n/es.json +++ b/assets/i18n/es.json @@ -35,7 +35,8 @@ "search": { "hint-text": "¡Por favor, busca ampliamente!~", - "all": "Todo" + "all": "Todo", + "filter": "Filtro" }, "extension": { @@ -63,6 +64,9 @@ "repo-url-subtitle": "Obtener el URL del repositorio para extensiones", "tmdb-key": "Clave de la API de TMDB", "tmdb-key-subtitle": "Obtener la clave de la API para los metadatos de TMDB", + "bt-server": "Servidor BT", + "bt-server-subtitle": "El servidor BT es un componente necesario para la reproducción en línea de torrents", + "bt-server-manager": "Gestionar", "upgrade": "Actualizar aplicación", "upgrade-subtitle": "versión: {version}", "upgrade-training": "Comprobar", @@ -76,6 +80,9 @@ "theme-dark": "Oscuro", "nsfw": "NSFW", "nsfw-subtitle": "Mostrar contenido NSFW", + "external-player": "Reproductor de vídeo preferido", + "external-player-subtitle": "Actualmente, el reproductor preferido es {player}", + "external-player-builtin": "Incorporado", "language-subtitle": "Cambia el idioma de la aplicación", "extension-log": "Ventana de registro de extensiones", "extension-log-subtitle": "Usado para depurar extensiones", @@ -88,6 +95,8 @@ "license-subtitle": "Licencia" }, + "external-player-launching": "Lanzando {player}", + "detail": { "favorite": "Favorito", "favorited": "Favorito", @@ -96,7 +105,10 @@ "overview": "Descripción general", "cast": "Reparto", "additional-info": "Información adicional", - "get-lastest-data-error": "No se pudieron obtener los datos más recientes" + "get-lastest-data-error": "No se pudieron obtener los datos más recientes", + "modify-tmdb-binding": "Modificar enlace a TMDB", + "no-tmdb-data": "No hay datos de TMDB coincidentes, por favor, enlaza los datos tu mismo/a", + "tmdb-key-missing": "Falta la clave de la API de TMDB, por favor, rellenala en los ajustes" }, "video": { @@ -108,7 +120,8 @@ "subtitle-none": "Sin subtítulos", "subtitle": "Subtítulos", "subtitle-change": "Cambiar subtítulos {title}", - "subtitle-file": "Archivo de subtítulos" + "subtitle-file": "Archivo de subtítulos", + "torrent-downloading": "Descargando torrent" }, "comic-settings": { @@ -171,5 +184,16 @@ "genres": "Géneros", "runtime": "Tiempo de ejecución", "languages": "Idiomas" + }, + + "bt-server": { + "not-installed": "BT-Server no está instalado", + "running": "BT-Server está ejecutándose", + "stopped": "BT-Server está detenido", + "version": "Versión {version}", + "remote-version": "Versión remota {version}", + "stop": "Parar", + "upgrade": "Mejorar", + "start": "Iniciar" } } diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json index 3704e96f..a790f645 100644 --- a/assets/i18n/ja.json +++ b/assets/i18n/ja.json @@ -21,7 +21,7 @@ "retry": "リトライ", "next": "次", "previous": "前", - "show-all": "全て表示" + "show-all": "全て表示", "delete": "削除", "delete-all": "全て削除" }, @@ -29,13 +29,13 @@ "home": { "continue-watching": "続ける", "favorite": "お気に入り", - "no-record": "お気に入りや閲覧記録はありません" + "no-record": "お気に入りや閲覧記録はありません", "watched": "視聴済み {ep}" }, "search": { "hint-text": "検索を使ってください~!", - "all": "全て" + "all": "全て", "filter": "フィルター" }, @@ -105,7 +105,7 @@ "overview": "概要", "cast": "キャスト", "additional-info": "追加情報", - "get-lastest-data-error": "最新データの取得に失敗しました" + "get-lastest-data-error": "最新データの取得に失敗しました", "modify-tmdb-binding": "TMDBバインディングを変更", "no-tmdb-data": "TMDBデータは一致していません。自分でデータをバインドしてください", "tmdb-key-missing": "TMDB APIキーがありません、設定から入力してください" @@ -120,7 +120,7 @@ "subtitle-none": "字幕なし", "subtitle": "字幕", "subtitle-change": "字幕を変更 {title}", - "subtitle-file": "字幕ファイル" + "subtitle-file": "字幕ファイル", "torrent-downloading": "Torrentでダウンロード" }, @@ -184,7 +184,7 @@ "genres": "ジャンル", "runtime": "実行時間", "languages": "言語" - }, + }, "bt-server": { "not-installed": "BTサーバーがインストールされていません", diff --git a/assets/i18n/ru.json b/assets/i18n/ru.json index 0be6d1c3..135203ab 100644 --- a/assets/i18n/ru.json +++ b/assets/i18n/ru.json @@ -64,9 +64,9 @@ "repo-url-subtitle": "Получить URL репозитория для расширений", "tmdb-key": "Ключ API TMDB", "tmdb-key-subtitle": "Получить ключ API для метаданных TMDB", - "bt-server": "BT Сервер", - "bt-server-subtitle": "BT Сервер — необходимый компонент для онлайн-воспроизведения сидов BT", - "bt-server-manager": "Менеджер", + "bt-server": "Сервер BT", + "bt-server-subtitle": "Сервер BT является необходимым компонентом для онлайн-воспроизведения торрентов", + "bt-server-manager": "управлять", "upgrade": "Обновление ПО", "upgrade-subtitle": "версия: {version}", "upgrade-training": "Проверить наличие обновлений", @@ -188,9 +188,12 @@ "bt-server": { "not-installed": "BT-Сервер не установлен", - "running": "BT-Сервер запущен", + "running": "BT-Сервер работает", "stopped": "BT-Сервер остановлен", "version": "Версия {version}", - "start": "Запуск" + "remote-version": "Удаленная версия {version}", + "stop": "Остановить", + "upgrade": "Обновить", + "start": "Запустить" } } diff --git a/assets/i18n/ryu.json b/assets/i18n/ryu.json index dd0492df..38021c11 100644 --- a/assets/i18n/ryu.json +++ b/assets/i18n/ryu.json @@ -21,7 +21,7 @@ "retry": "リトライ", "next": "ちぎ", "previous": "まい", - "show-all": "まじりひょうじ" + "show-all": "まじりひょうじ", "delete": "さちゅるじょ", "delete-all": "まじりさちゅるじょ" }, @@ -29,13 +29,13 @@ "home": { "continue-watching": "ちづきーん", "favorite": "うきーがいー", - "no-record": "うきーにいりてぃがろーいちらんきるこーあいびらん" + "no-record": "うきーにいりてぃがろーいちらんきるこーあいびらん", "watched": "しちょうじみ {ep}" }, "search": { "hint-text": "きんさくちかてぃくぃみそーれー~!", - "all": "まじり" + "all": "まじり", "filter": "フィルター" }, @@ -105,7 +105,7 @@ "overview": "がいよう", "cast": "キャスト", "additional-info": "ちいかじょうほう", - "get-lastest-data-error": "さいしんデータぬしゅとぅくんかいしっぺーさびたん" + "get-lastest-data-error": "さいしんデータぬしゅとぅくんかいしっぺーさびたん", "modify-tmdb-binding": "TMDBバインディングへいるかん", "no-tmdb-data": "TMDBデーターいっちさびらん。じぶんっしデータバインドしみそーれー", "tmdb-key-missing": "TMDB APIキーぬあいびらん、しっていからんかいゅうりょくしみそーれー" @@ -120,7 +120,7 @@ "subtitle-none": "じまちゅんなし", "subtitle": "じまちゅん", "subtitle-change": "じまちゅんへいるかん {title}", - "subtitle-file": "じまちゅるファイル" + "subtitle-file": "じまちゅるファイル", "torrent-downloading": "Torrentっしダウンロード" }, @@ -184,7 +184,7 @@ "genres": "ジャンル", "runtime": "じっこうじがん", "languages": "ぎんぐ" - }, + }, "bt-server": { "not-installed": "BTサーバーぬインストールさりやびらん", diff --git a/assets/i18n/uk.json b/assets/i18n/uk.json index 207cef64..b7b10193 100644 --- a/assets/i18n/uk.json +++ b/assets/i18n/uk.json @@ -64,9 +64,9 @@ "repo-url-subtitle": "Отримати URL репозиторія для розширень", "tmdb-key": "Ключ API TMDB", "tmdb-key-subtitle": "Отримати ключ API для метаданих TMDB", - "bt-server": "BT Сервер", - "bt-server-subtitle": "BT Сервер – необхідний компонент для онлайн-відтворення сидів BT", - "bt-server-manager": "Менеджер", + "bt-server": "Сервер BT", + "bt-server-subtitle": "Сервер BT є необхідним компонентом для онлайн-відтворення торрентів", + "bt-server-manager": "управляти", "upgrade": "Оновлення ПЗ", "upgrade-subtitle": "версія: {version}", "upgrade-training": "Перевірити наявність оновлень", @@ -186,11 +186,14 @@ "languages": "Мови" }, - "bt-server": { + "bt-server": { "not-installed": "BT-Сервер не встановлено", - "running": "BT-Сервер запущено", + "running": "BT-Сервер працює", "stopped": "BT-Сервер зупинено", "version": "Версія {version}", - "start": "Запуск" + "remote-version": "Віддалена версія {version}", + "stop": "Зупинити", + "upgrade": "Оновити", + "start": "Запустити" } } diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json index 076ccc15..14b0a436 100644 --- a/assets/i18n/zh.json +++ b/assets/i18n/zh.json @@ -78,6 +78,7 @@ "theme-system": "跟随系统", "theme-light": "浅色", "theme-dark": "深色", + "theme-black": "黑色", "nsfw": "NSFW", "nsfw-subtitle": "显示 NSFW 内容", "external-player": "优先使用的视频播放器", @@ -191,6 +192,9 @@ "running": "BT-Server 正在运行", "stopped": "BT-Server 已停止", "version": "版本 {version}", + "remote-version": "远程版本 {version}", + "stop": "停止", + "upgrade": "升级", "start": "启动" } } diff --git a/lib/controller.dart b/lib/controller.dart index 3822caf3..4a7b4b24 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -13,12 +13,50 @@ class ApplicationController extends GetxController { super.onInit(); } + ThemeData get currentThemeData { + switch (themeText.value) { + case "light": + return ThemeData.light(useMaterial3: true); + case "dark": + return ThemeData.dark(useMaterial3: true); + case "black": + return ThemeData.dark( + useMaterial3: true, + ).copyWith( + scaffoldBackgroundColor: Colors.black, + canvasColor: Colors.black, + cardColor: Colors.black, + dialogBackgroundColor: Colors.black, + primaryColor: Colors.black, + hintColor: Colors.black, + primaryColorDark: Colors.black, + primaryColorLight: Colors.black, + colorScheme: const ColorScheme.dark( + primary: Colors.white, + onBackground: Colors.white, + onSecondary: Colors.white, + onSurface: Colors.white, + secondary: Colors.grey, + surface: Colors.black, + background: Colors.black, + onPrimary: Colors.black, + primaryContainer: Color.fromARGB(255, 31, 31, 31), + surfaceTint: Colors.black, + ), + ); + default: + return ThemeData.light(useMaterial3: true); + } + } + ThemeMode get theme { switch (themeText.value) { case "light": return ThemeMode.light; case "dark": return ThemeMode.dark; + case "black": + return ThemeMode.light; default: return ThemeMode.system; } diff --git a/lib/main.dart b/lib/main.dart index 2e66925a..9ea7ca5d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -95,7 +95,7 @@ class _MainAppState extends fluent.State { title: "Miru", debugShowCheckedModeBanner: false, themeMode: c.theme, - theme: ThemeData(useMaterial3: true), + theme: c.currentThemeData, darkTheme: ThemeData.dark(useMaterial3: true), home: const AndroidMainPage(), localizationsDelegates: [ diff --git a/lib/models/extension.dart b/lib/models/extension.dart index ad1382b6..7aed3d3a 100644 --- a/lib/models/extension.dart +++ b/lib/models/extension.dart @@ -75,14 +75,14 @@ class ExtensionListItem { ExtensionListItem({ required this.title, required this.url, - required this.cover, + this.cover, this.update, this.headers, }); final String title; final String url; - final String cover; + final String? cover; final String? update; late Map? headers; @@ -98,14 +98,14 @@ class ExtensionListItem { class ExtensionDetail { ExtensionDetail({ required this.title, - required this.cover, + this.cover, this.desc, this.episodes, this.headers, }); final String title; - final String cover; + final String? cover; final String? desc; final List? episodes; late Map? headers; @@ -153,11 +153,13 @@ class ExtensionBangumiWatch { required this.url, this.subtitles, this.headers, + this.audioTrack, }); final ExtensionWatchBangumiType type; final String url; final List? subtitles; late Map? headers; + late String? audioTrack; factory ExtensionBangumiWatch.fromJson(Map json) => _$ExtensionBangumiWatchFromJson(json); diff --git a/lib/models/extension.g.dart b/lib/models/extension.g.dart index 28a9b6a7..98ba3920 100644 --- a/lib/models/extension.g.dart +++ b/lib/models/extension.g.dart @@ -64,7 +64,7 @@ ExtensionListItem _$ExtensionListItemFromJson(Map json) => ExtensionListItem( title: json['title'] as String, url: json['url'] as String, - cover: json['cover'] as String, + cover: json['cover'] as String?, update: json['update'] as String?, headers: (json['headers'] as Map?)?.map( (k, e) => MapEntry(k, e as String), @@ -83,7 +83,7 @@ Map _$ExtensionListItemToJson(ExtensionListItem instance) => ExtensionDetail _$ExtensionDetailFromJson(Map json) => ExtensionDetail( title: json['title'] as String, - cover: json['cover'] as String, + cover: json['cover'] as String?, desc: json['desc'] as String?, episodes: (json['episodes'] as List?) ?.map( @@ -143,6 +143,7 @@ ExtensionBangumiWatch _$ExtensionBangumiWatchFromJson( headers: (json['headers'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), + audioTrack: json['audioTrack'] as String?, ); Map _$ExtensionBangumiWatchToJson( @@ -152,6 +153,7 @@ Map _$ExtensionBangumiWatchToJson( 'url': instance.url, 'subtitles': instance.subtitles, 'headers': instance.headers, + 'audioTrack': instance.audioTrack, }; const _$ExtensionWatchBangumiTypeEnumMap = { diff --git a/lib/models/favorite.dart b/lib/models/favorite.dart index a8e53628..a6cf6831 100644 --- a/lib/models/favorite.dart +++ b/lib/models/favorite.dart @@ -12,6 +12,6 @@ class Favorite { @Enumerated(EnumType.name) late ExtensionType type; late String title; - late String cover; + String? cover; DateTime date = DateTime.now(); } diff --git a/lib/models/favorite.g.dart b/lib/models/favorite.g.dart index f9de1c44..e5e87e54 100644 --- a/lib/models/favorite.g.dart +++ b/lib/models/favorite.g.dart @@ -88,7 +88,12 @@ int _favoriteEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; - bytesCount += 3 + object.cover.length * 3; + { + final value = object.cover; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.package.length * 3; bytesCount += 3 + object.title.length * 3; bytesCount += 3 + object.type.name.length * 3; @@ -117,7 +122,7 @@ Favorite _favoriteDeserialize( Map> allOffsets, ) { final object = Favorite(); - object.cover = reader.readString(offsets[0]); + object.cover = reader.readStringOrNull(offsets[0]); object.date = reader.readDateTime(offsets[1]); object.id = id; object.package = reader.readString(offsets[2]); @@ -137,7 +142,7 @@ P _favoriteDeserializeProp

( ) { switch (propertyId) { case 0: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 1: return (reader.readDateTime(offset)) as P; case 2: @@ -344,8 +349,24 @@ extension FavoriteQueryWhere on QueryBuilder { extension FavoriteQueryFilter on QueryBuilder { + QueryBuilder coverIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'cover', + )); + }); + } + + QueryBuilder coverIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'cover', + )); + }); + } + QueryBuilder coverEqualTo( - String value, { + String? value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { @@ -358,7 +379,7 @@ extension FavoriteQueryFilter } QueryBuilder coverGreaterThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -373,7 +394,7 @@ extension FavoriteQueryFilter } QueryBuilder coverLessThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -388,8 +409,8 @@ extension FavoriteQueryFilter } QueryBuilder coverBetween( - String lower, - String upper, { + String? lower, + String? upper, { bool includeLower = true, bool includeUpper = true, bool caseSensitive = true, @@ -1319,7 +1340,7 @@ extension FavoriteQueryProperty }); } - QueryBuilder coverProperty() { + QueryBuilder coverProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'cover'); }); diff --git a/lib/models/history.dart b/lib/models/history.dart index 5a022a3a..20690f6a 100644 --- a/lib/models/history.dart +++ b/lib/models/history.dart @@ -10,7 +10,7 @@ class History { late String package; late String url; // 截图,保存封面地址 - late String cover; + String? cover; @Enumerated(EnumType.name) late ExtensionType type; // 不同线路 diff --git a/lib/models/history.g.dart b/lib/models/history.g.dart index 15477ca4..1bc506e1 100644 --- a/lib/models/history.g.dart +++ b/lib/models/history.g.dart @@ -113,7 +113,12 @@ int _historyEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; - bytesCount += 3 + object.cover.length * 3; + { + final value = object.cover; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.episodeTitle.length * 3; bytesCount += 3 + object.package.length * 3; bytesCount += 3 + object.progress.length * 3; @@ -150,7 +155,7 @@ History _historyDeserialize( Map> allOffsets, ) { final object = History(); - object.cover = reader.readString(offsets[0]); + object.cover = reader.readStringOrNull(offsets[0]); object.date = reader.readDateTime(offsets[1]); object.episodeGroupId = reader.readLong(offsets[2]); object.episodeId = reader.readLong(offsets[3]); @@ -174,7 +179,7 @@ P _historyDeserializeProp

( ) { switch (propertyId) { case 0: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 1: return (reader.readDateTime(offset)) as P; case 2: @@ -391,8 +396,24 @@ extension HistoryQueryWhere on QueryBuilder { extension HistoryQueryFilter on QueryBuilder { + QueryBuilder coverIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'cover', + )); + }); + } + + QueryBuilder coverIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'cover', + )); + }); + } + QueryBuilder coverEqualTo( - String value, { + String? value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { @@ -405,7 +426,7 @@ extension HistoryQueryFilter } QueryBuilder coverGreaterThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -420,7 +441,7 @@ extension HistoryQueryFilter } QueryBuilder coverLessThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -435,8 +456,8 @@ extension HistoryQueryFilter } QueryBuilder coverBetween( - String lower, - String upper, { + String? lower, + String? upper, { bool includeLower = true, bool includeUpper = true, bool caseSensitive = true, @@ -2020,7 +2041,7 @@ extension HistoryQueryProperty }); } - QueryBuilder coverProperty() { + QueryBuilder coverProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'cover'); }); diff --git a/lib/pages/bt_dialog/controller.dart b/lib/pages/bt_dialog/controller.dart index 772ab2ac..891cf0ca 100644 --- a/lib/pages/bt_dialog/controller.dart +++ b/lib/pages/bt_dialog/controller.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/scheduler.dart'; import 'package:get/get.dart'; import 'package:miru_app/pages/main/controller.dart'; @@ -9,6 +10,8 @@ class BTDialogController extends GetxController { final isInstalled = false.obs; final isDownloading = false.obs; final progress = 0.0.obs; + final hasUpdate = false.obs; + final remoteVersion = "".obs; final _mainController = Get.find(); late final isRuning = _mainController.btServerisRunning; @@ -17,18 +20,27 @@ class BTDialogController extends GetxController { @override void onInit() { super.onInit(); + ever( + isRuning, + (callback) { + hasUpdate.value = version.value != remoteVersion.value; + }, + ); SchedulerBinding.instance.addPostFrameCallback((_) async { isInstalled.value = await BTServerUtils.isInstalled(); + remoteVersion.value = await BTServerUtils.getRemoteVersion(); }); } downloadOrUpgradeServer(BuildContext context) async { + progress.value = 0; + BTServerUtils.stopServer(); + isInstalled.value = false; isDownloading.value = true; try { await BTServerUtils.downloadLatestBTServer( onReceiveProgress: (p0, p1) { progress.value = p0 / p1; - print(progress.value); }, ); } catch (e) { @@ -36,6 +48,7 @@ class BTDialogController extends GetxController { showPlatformSnackbar( context: context, content: e.toString(), + severity: fluent.InfoBarSeverity.error, ); } finally { isDownloading.value = false; diff --git a/lib/pages/bt_dialog/view.dart b/lib/pages/bt_dialog/view.dart index 54a4d392..19ce14f0 100644 --- a/lib/pages/bt_dialog/view.dart +++ b/lib/pages/bt_dialog/view.dart @@ -54,22 +54,77 @@ class _BTDialogState extends State { Text("bt-server.stopped".i18n), const SizedBox(height: 16), if (c.isRuning.value) - Text( - FlutterI18n.translate( - context, - 'bt-server.version', - translationParams: { - "version": c.version.value, - }, + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Text( + FlutterI18n.translate( + context, + 'bt-server.version', + translationParams: { + "version": c.version.value, + }, + ), + ), + const SizedBox(width: 8), + if (c.hasUpdate.value) + Text( + FlutterI18n.translate( + context, + 'bt-server.remote-version', + translationParams: { + "version": c.remoteVersion.value, + }, + ), + ), + ], ), - ) - else - PlatformFilledButton( - child: Text("bt-server.start".i18n), - onPressed: () { - BTServerUtils.startServer(); - }, ), + Wrap( + children: [ + if (c.isRuning.value) ...[ + Padding( + padding: const EdgeInsets.only(right: 8), + child: PlatformFilledButton( + child: Text('bt-server.stop'.i18n), + onPressed: () { + BTServerUtils.stopServer(); + }, + ), + ), + // 升级按钮 + if (c.hasUpdate.value) + Padding( + padding: const EdgeInsets.only(right: 8), + child: PlatformFilledButton( + child: Text("bt-server.upgrade".i18n), + onPressed: () { + c.downloadOrUpgradeServer(context); + }, + ), + ), + ] else + Padding( + padding: const EdgeInsets.only(right: 8), + child: PlatformFilledButton( + child: Text("bt-server.start".i18n), + onPressed: () { + BTServerUtils.startServer(); + }, + ), + ), + if (c.isInstalled.value) + // 卸载 + PlatformFilledButton( + child: Text("common.uninstall".i18n), + onPressed: () async { + await BTServerUtils.uninstall(); + c.isInstalled.value = false; + }, + ), + ], + ), ], ); }); diff --git a/lib/pages/detail/controller.dart b/lib/pages/detail/controller.dart index 59e92015..cdcd194b 100644 --- a/lib/pages/detail/controller.dart +++ b/lib/pages/detail/controller.dart @@ -55,12 +55,12 @@ class DetailPageController extends GetxController { TMDBDetail? get tmdbDetail => tmdb.value; set tmdbDetail(TMDBDetail? value) => tmdb.value = value; - String get backgorund { - String bg = ''; + String? get backgorund { + String? bg; if (tmdbDetail != null && tmdbDetail!.backdrop != null) { bg = TmdbApi.getImageUrl(tmdbDetail!.backdrop!) ?? ''; } else { - bg = detail?.cover ?? ''; + bg = detail?.cover; } return bg; } @@ -151,7 +151,7 @@ class DetailPageController extends GetxController { // 判断是否有 key if (MiruStorage.getSetting(SettingKey.tmdbKay) == "") { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: 'detail.tmdb-key-missing'.i18n, severity: fluent.InfoBarSeverity.error, ); @@ -165,7 +165,7 @@ class DetailPageController extends GetxController { )); } else { data = await fluent.showDialog( - context: cuurentContext, + context: currentContext, builder: (context) => TMDBBinding(title: detail!.title), ); } @@ -198,21 +198,21 @@ class DetailPageController extends GetxController { // 弹出错误信息 if (runtime.value == null) { final content = FlutterI18n.translate( - cuurentContext, + currentContext, 'common.extension-missing', translationParams: { 'package': package, }, ); showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: content, severity: fluent.InfoBarSeverity.error, ); throw content; } else { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, title: 'detail.get-lastest-data-error'.i18n, content: e.toString().split('\n')[0], severity: fluent.InfoBarSeverity.error, @@ -293,7 +293,7 @@ class DetailPageController extends GetxController { ); } catch (e) { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: e.toString().split('\n')[0], severity: fluent.InfoBarSeverity.error, ); @@ -311,9 +311,9 @@ class DetailPageController extends GetxController { ) async { if (runtime.value == null) { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: FlutterI18n.translate( - cuurentContext, + currentContext, 'common.extension-missing', translationParams: { 'package': package, @@ -329,9 +329,9 @@ class DetailPageController extends GetxController { if (player != 'built-in') { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: FlutterI18n.translate( - cuurentContext, + currentContext, 'external-player-launching', translationParams: { 'player': player, @@ -344,7 +344,7 @@ class DetailPageController extends GetxController { as ExtensionBangumiWatch; } catch (e) { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: e.toString().split('\n')[0], severity: fluent.InfoBarSeverity.error, ); @@ -359,7 +359,7 @@ class DetailPageController extends GetxController { return; } catch (e) { showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: e.toString().split('\n')[0], severity: fluent.InfoBarSeverity.error, ); diff --git a/lib/pages/detail/view.dart b/lib/pages/detail/view.dart index b97e40a3..de670672 100644 --- a/lib/pages/detail/view.dart +++ b/lib/pages/detail/view.dart @@ -17,6 +17,7 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/utils/layout.dart'; import 'package:miru_app/widgets/cache_network_image.dart'; import 'package:miru_app/widgets/card_tile.dart'; +import 'package:miru_app/widgets/cover.dart'; import 'package:miru_app/widgets/platform_widget.dart'; import 'package:miru_app/widgets/progress.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -26,11 +27,11 @@ class DetailPage extends StatefulWidget { Key? key, required this.url, required this.package, - this.heroTag, + this.tag, }) : super(key: key); final String url; final String package; - final String? heroTag; + final String? tag; @override State createState() => _DetailPageState(); @@ -45,15 +46,18 @@ class _DetailPageState extends State { DetailPageController( package: widget.package, url: widget.url, - heroTag: widget.heroTag, + heroTag: widget.tag, ), + tag: widget.tag, ); super.initState(); } @override void dispose() { - Get.delete(); + Get.delete( + tag: widget.tag, + ); super.dispose(); } @@ -94,7 +98,9 @@ class _DetailPageState extends State { c.detail?.title ?? '', controller: c.scrollController, ), - flexibleSpace: const DetailAppbarflexibleSpace(), + flexibleSpace: DetailAppbarflexibleSpace( + tag: widget.tag, + ), bottom: TabBar( tabs: tabs, ), @@ -138,8 +144,13 @@ class _DetailPageState extends State { padding: const EdgeInsets.all(8), child: TabBarView( children: [ - if (!LayoutUtils.isTablet) const DetailEpisodes(), - const DetailOverView(), + if (!LayoutUtils.isTablet) + DetailEpisodes( + tag: widget.tag, + ), + DetailOverView( + tag: widget.tag, + ), if (c.type == ExtensionType.bangumi) Obx(() { if (c.tmdbDetail == null || c.tmdbDetail!.casts.isEmpty) { @@ -204,8 +215,11 @@ class _DetailPageState extends State { return Row( children: [ Expanded(child: content), - const Expanded( - child: SafeArea(child: DetailEpisodes()), + Expanded( + child: SafeArea( + child: DetailEpisodes( + tag: widget.tag, + )), ), ], ); @@ -232,11 +246,10 @@ class _DetailPageState extends State { return Stack( children: [ Animate( - child: CacheNetWorkImage( - c.backgorund, - width: double.infinity, - height: double.infinity, - headers: c.detail?.headers, + child: Cover( + alt: c.detail?.title ?? '', + url: c.backgorund, + noText: true, ), ).blur( begin: const Offset(10, 10), @@ -260,24 +273,25 @@ class _DetailPageState extends State { height: 330, child: Row( children: [ - if (constraints.maxWidth > 600) ...[ - Hero( - tag: c.heroTag ?? '', - child: Container( - width: 230, - height: double.infinity, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), - child: CacheNetWorkImage( - c.detail?.cover ?? '', - headers: c.detail?.headers, + if (c.detail!.cover != null) + if (constraints.maxWidth > 600) ...[ + Hero( + tag: c.heroTag ?? '', + child: Container( + width: 230, + height: double.infinity, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: CacheNetWorkImage( + c.detail?.cover ?? '', + headers: c.detail?.headers, + ), ), ), - ), - const SizedBox(width: 30), - ], + const SizedBox(width: 30), + ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/detail/widgets/detail_appbar_flexible_space.dart b/lib/pages/detail/widgets/detail_appbar_flexible_space.dart index 7b17b33a..9071aadf 100644 --- a/lib/pages/detail/widgets/detail_appbar_flexible_space.dart +++ b/lib/pages/detail/widgets/detail_appbar_flexible_space.dart @@ -5,19 +5,23 @@ import 'package:miru_app/pages/detail/widgets/detail_continue_play.dart'; import 'package:miru_app/pages/detail/widgets/detail_extension_tile.dart'; import 'package:miru_app/pages/detail/widgets/detail_favorite_button.dart'; import 'package:miru_app/widgets/cache_network_image.dart'; +import 'package:miru_app/widgets/cover.dart'; class DetailAppbarflexibleSpace extends StatefulWidget { const DetailAppbarflexibleSpace({ Key? key, + this.tag, }) : super(key: key); + final String? tag; + @override State createState() => _DetailAppbarflexibleSpaceState(); } class _DetailAppbarflexibleSpaceState extends State { - final DetailPageController c = Get.find(); + late DetailPageController c = Get.find(tag: widget.tag); double _offset = 1; @@ -43,6 +47,16 @@ class _DetailAppbarflexibleSpaceState extends State { @override Widget build(BuildContext context) { + bool needShowCover() { + if (c.isLoading.value) { + return true; + } + if (c.data.value?.cover != null) { + return true; + } + return false; + } + return Obx( () => Opacity( opacity: _scrollListener(), @@ -53,12 +67,10 @@ class _DetailAppbarflexibleSpaceState extends State { width: double.infinity, child: c.isLoading.value ? const SizedBox.shrink() - : CacheNetWorkImage( - c.backgorund, - height: double.infinity, - fit: BoxFit.cover, - width: double.infinity, - headers: c.detail?.headers, + : Cover( + alt: c.data.value?.title ?? '', + url: c.backgorund, + noText: true, ), ), Positioned.fill( @@ -83,23 +95,24 @@ class _DetailAppbarflexibleSpaceState extends State { right: 20, child: Row( children: [ - Hero( - tag: c.heroTag ?? '', - child: Card( - clipBehavior: Clip.antiAlias, - child: SizedBox( - height: 150, - width: 100, - child: c.isLoading.value - ? const Center(child: CircularProgressIndicator()) - : CacheNetWorkImage( - c.data.value!.cover, - fit: BoxFit.cover, - headers: c.detail?.headers, - ), + if (needShowCover()) + Hero( + tag: c.heroTag ?? '', + child: Card( + clipBehavior: Clip.antiAlias, + child: SizedBox( + height: 150, + width: 100, + child: c.isLoading.value + ? const Center(child: CircularProgressIndicator()) + : CacheNetWorkImage( + c.data.value?.cover ?? '', + fit: BoxFit.cover, + headers: c.detail?.headers, + ), + ), ), ), - ), Expanded( child: Container( padding: const EdgeInsets.only(left: 20), @@ -112,7 +125,9 @@ class _DetailAppbarflexibleSpaceState extends State { style: Get.theme.textTheme.titleLarge, ), const SizedBox(height: 10), - const DetailExtensionTile(), + DetailExtensionTile( + tag: widget.tag, + ), ], ), ), @@ -120,7 +135,7 @@ class _DetailAppbarflexibleSpaceState extends State { ], ), ), - const Positioned( + Positioned( top: null, left: 20, right: 20, @@ -129,14 +144,18 @@ class _DetailAppbarflexibleSpaceState extends State { children: [ Expanded( flex: 4, - child: DetailContinuePlay(), + child: DetailContinuePlay( + tag: widget.tag, + ), ), - SizedBox( + const SizedBox( width: 10, ), Expanded( flex: 3, - child: DetailFavoriteButton(), + child: DetailFavoriteButton( + tag: widget.tag, + ), ) ], ), diff --git a/lib/pages/detail/widgets/detail_continue_play.dart b/lib/pages/detail/widgets/detail_continue_play.dart index d66cb87b..b7b66ca7 100644 --- a/lib/pages/detail/widgets/detail_continue_play.dart +++ b/lib/pages/detail/widgets/detail_continue_play.dart @@ -10,13 +10,16 @@ import 'package:miru_app/widgets/platform_widget.dart'; class DetailContinuePlay extends StatefulWidget { const DetailContinuePlay({ Key? key, + this.tag, }) : super(key: key); + final String? tag; + @override State createState() => _DetailContinuePlayState(); } class _DetailContinuePlayState extends State { - late DetailPageController c = Get.find(); + late DetailPageController c = Get.find(tag: widget.tag); Widget _buildAndroid(BuildContext context) { return Obx(() { @@ -119,13 +122,19 @@ class _DetailContinuePlayState extends State { children: [ const Icon(fluent.FluentIcons.play), const SizedBox(width: 5), - Text( - FlutterI18n.translate( - context, - 'detail.continue-watching', - translationParams: { - 'episode': history.episodeTitle, - }, + Container( + constraints: const BoxConstraints( + maxWidth: 150, + ), + child: Text( + FlutterI18n.translate( + context, + 'detail.continue-watching', + translationParams: { + 'episode': history.episodeTitle, + }, + ), + maxLines: 1, ), ) ], diff --git a/lib/pages/detail/widgets/detail_episodes.dart b/lib/pages/detail/widgets/detail_episodes.dart index 215ebffa..a6bf703f 100644 --- a/lib/pages/detail/widgets/detail_episodes.dart +++ b/lib/pages/detail/widgets/detail_episodes.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:miru_app/models/extension.dart'; import 'package:miru_app/pages/detail/controller.dart'; import 'package:miru_app/pages/detail/widgets/detail_continue_play.dart'; +import 'package:miru_app/utils/miru_storage.dart'; import 'package:miru_app/widgets/card_tile.dart'; import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/widgets/platform_widget.dart'; @@ -12,17 +13,20 @@ import 'package:miru_app/widgets/platform_widget.dart'; class DetailEpisodes extends StatefulWidget { const DetailEpisodes({ Key? key, + this.tag, }) : super(key: key); + final String? tag; @override State createState() => _DetailEpisodesState(); } class _DetailEpisodesState extends State { - late DetailPageController c = Get.find(); + late DetailPageController c = Get.find(tag: widget.tag); List>? comboBoxItems; List>? dropdownItems; late List episodes = []; + late String listMode = MiruStorage.getSetting(SettingKey.listMode); Widget _buildAndroidEpisodes(BuildContext context) { return Column( @@ -101,8 +105,23 @@ class _DetailEpisodesState extends State { } else { episodesString = 'reader.chapters'.i18n; } - return CardTile( + + Widget cardTile(Widget child) { + return CardTile( title: episodesString, + leading: fluent.IconButton( + icon: Icon( + listMode == "grid" + ? fluent.FluentIcons.view_list + : fluent.FluentIcons.grid_view_medium, + ), + onPressed: () { + setState(() { + listMode == "grid" ? listMode = "list" : listMode = "grid"; + MiruStorage.setSetting(SettingKey.listMode, listMode); + }); + }, + ), trailing: Row( children: [ const DetailContinuePlay(), @@ -118,12 +137,20 @@ class _DetailEpisodesState extends State { ) ], ), - child: LayoutBuilder(builder: (context, constraints) { - return Container( - constraints: const BoxConstraints( - maxHeight: 500, - ), - child: GridView.builder( + child: Container( + constraints: const BoxConstraints( + maxHeight: 500, + ), + child: child, + ), + ); + } + + if (listMode == "grid") { + return cardTile( + LayoutBuilder( + builder: (context, constraints) { + return GridView.builder( shrinkWrap: true, itemCount: episodes.isEmpty ? 0 @@ -149,9 +176,33 @@ class _DetailEpisodesState extends State { }, ); }, - ), + ); + }, + ), + ); + } + + return cardTile( + ListView.builder( + shrinkWrap: true, + padding: const EdgeInsets.all(0), + itemCount: + episodes.isEmpty ? 0 : episodes[c.selectEpGroup.value].urls.length, + itemBuilder: (context, index) { + return fluent.ListTile( + title: Text(episodes[c.selectEpGroup.value].urls[index].name), + onPressed: () { + c.goWatch( + context, + episodes[c.selectEpGroup.value].urls, + index, + c.selectEpGroup.value, + ); + }, ); - })); + }, + ), + ); } @override diff --git a/lib/pages/detail/widgets/detail_extension_tile.dart b/lib/pages/detail/widgets/detail_extension_tile.dart index f3673101..7212c0ab 100644 --- a/lib/pages/detail/widgets/detail_extension_tile.dart +++ b/lib/pages/detail/widgets/detail_extension_tile.dart @@ -6,11 +6,16 @@ import 'package:miru_app/utils/extension.dart'; import 'package:miru_app/widgets/cache_network_image.dart'; class DetailExtensionTile extends StatelessWidget { - const DetailExtensionTile({Key? key}) : super(key: key); + const DetailExtensionTile({ + Key? key, + this.tag, + }) : super(key: key); + + final String? tag; @override Widget build(BuildContext context) { - final c = Get.find(); + final c = Get.find(tag: tag); return Obx(() { if (c.extension == null) { return Text(FlutterI18n.translate( diff --git a/lib/pages/detail/widgets/detail_favorite_button.dart b/lib/pages/detail/widgets/detail_favorite_button.dart index 2eafe210..60467b8a 100644 --- a/lib/pages/detail/widgets/detail_favorite_button.dart +++ b/lib/pages/detail/widgets/detail_favorite_button.dart @@ -6,7 +6,11 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/widgets/platform_widget.dart'; class DetailFavoriteButton extends StatefulWidget { - const DetailFavoriteButton({Key? key}) : super(key: key); + const DetailFavoriteButton({ + Key? key, + this.tag, + }) : super(key: key); + final String? tag; @override fluent.State createState() => @@ -14,7 +18,7 @@ class DetailFavoriteButton extends StatefulWidget { } class _DetailFavoriteButtonState extends State { - final c = Get.find(); + late DetailPageController c = Get.find(tag: widget.tag); Widget _buildAndroid(BuildContext context) { return Obx( diff --git a/lib/pages/detail/widgets/detail_overview.dart b/lib/pages/detail/widgets/detail_overview.dart index 535b9e87..1ad7a33a 100644 --- a/lib/pages/detail/widgets/detail_overview.dart +++ b/lib/pages/detail/widgets/detail_overview.dart @@ -6,11 +6,16 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/widgets/cache_network_image.dart'; class DetailOverView extends StatelessWidget { - const DetailOverView({Key? key}) : super(key: key); + const DetailOverView({ + Key? key, + this.tag, + }) : super(key: key); + + final String? tag; @override Widget build(BuildContext context) { - final c = Get.find(); + final c = Get.find(tag: tag); return Padding( padding: const EdgeInsets.only( left: 16, diff --git a/lib/pages/extension_repo/controller.dart b/lib/pages/extension_repo/controller.dart index 74d23507..b8fff285 100644 --- a/lib/pages/extension_repo/controller.dart +++ b/lib/pages/extension_repo/controller.dart @@ -52,7 +52,7 @@ class ExtensionRepoPageController extends GetxController { if (Platform.isAndroid && extensions.isEmpty) { // ignore: use_build_context_synchronously showPlatformSnackbar( - context: cuurentContext, + context: currentContext, content: 'extension-repo.empty'.i18n, ); } diff --git a/lib/pages/extension_settings/view.dart b/lib/pages/extension_settings/view.dart index 8e270fae..faf8216d 100644 --- a/lib/pages/extension_settings/view.dart +++ b/lib/pages/extension_settings/view.dart @@ -162,15 +162,7 @@ class _ExtensionSettingsPageState extends State { const SizedBox(height: 30), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: GridView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 3, - crossAxisSpacing: 2, - mainAxisSpacing: 8, - ), + child: Wrap( children: [ InfoCard( icon: Icons.person, @@ -377,13 +369,7 @@ class _ExtensionSettingsPageState extends State { ], CardTile( title: 'extension-info.other-infomation'.i18n, - child: GridView( - gridDelegate: - const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - childAspectRatio: 3, - ), - shrinkWrap: true, + child: Wrap( children: [ InfoCard( icon: fluent.FluentIcons.contact, diff --git a/lib/pages/extension_settings/widgets/info_card.dart b/lib/pages/extension_settings/widgets/info_card.dart index 16dcb392..9426ee96 100644 --- a/lib/pages/extension_settings/widgets/info_card.dart +++ b/lib/pages/extension_settings/widgets/info_card.dart @@ -15,44 +15,51 @@ class InfoCard extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformWidget( - androidWidget: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - content, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 4), - Expanded( - child: Text( - title, - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), - )), - ], + androidWidget: Container( + width: 130, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SelectableText( + content, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + title, + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), ), - desktopWidget: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - icon, - size: 14, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title), - const SizedBox(height: 4), - Text(content), - ], + desktopWidget: Container( + width: 200, + margin: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + size: 14, ), - ) - ], + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + const SizedBox(height: 4), + Text(content), + ], + ), + ) + ], + ), ), ); } diff --git a/lib/pages/home/widgets/home_resent_card.dart b/lib/pages/home/widgets/home_resent_card.dart index 34cd9ae0..6f6d301d 100644 --- a/lib/pages/home/widgets/home_resent_card.dart +++ b/lib/pages/home/widgets/home_resent_card.dart @@ -10,6 +10,7 @@ import 'package:miru_app/models/history.dart'; import 'package:miru_app/pages/detail/view.dart'; import 'package:miru_app/pages/home/controller.dart'; import 'package:miru_app/router/router.dart'; +import 'package:miru_app/utils/color.dart'; import 'package:miru_app/utils/database.dart'; import 'package:miru_app/utils/extension.dart'; import 'package:miru_app/utils/extension_runtime.dart'; @@ -35,6 +36,7 @@ class _HomeRecentCardState extends State { final contextAttachKey = GlobalKey(); // 主要颜色 Color? primaryColor; + late bool noCover = widget.history.cover == null; @override void initState() { @@ -59,11 +61,11 @@ class _HomeRecentCardState extends State { } _genColor() async { - if (widget.history.type == ExtensionType.bangumi) { + if (widget.history.type == ExtensionType.bangumi || noCover) { return; } final paletteGenerator = await PaletteGenerator.fromImageProvider( - CachedNetworkImageProvider(widget.history.cover), + CachedNetworkImageProvider(widget.history.cover!), maximumColorCount: 2, ); @@ -97,7 +99,7 @@ class _HomeRecentCardState extends State { child: Stack( children: [ Image.file( - File(widget.history.cover), + File(widget.history.cover!), fit: BoxFit.cover, width: double.infinity, height: double.infinity, @@ -142,6 +144,7 @@ class _HomeRecentCardState extends State { color: Colors.white, fontSize: 12, ), + maxLines: 1, ), ], ), @@ -164,22 +167,25 @@ class _HomeRecentCardState extends State { } Widget _coverCard() { + if (widget.history.cover == null) {} + return Container( width: 350, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(8)), - // color: - // _paletteGenerator != null ? _paletteGenerator!.colors.first : null, - image: DecorationImage( - image: CachedNetworkImageProvider(widget.history.cover), - fit: BoxFit.cover, - colorFilter: primaryColor != null - ? ColorFilter.mode( - primaryColor!.withOpacity(0.9), - BlendMode.srcOver, - ) - : null, - ), + color: noCover ? ColorUtils.getColor(widget.history.title) : null, + image: noCover + ? null + : DecorationImage( + image: CachedNetworkImageProvider(widget.history.cover!), + fit: BoxFit.cover, + colorFilter: primaryColor != null + ? ColorFilter.mode( + primaryColor!.withOpacity(0.9), + BlendMode.srcOver, + ) + : null, + ), ), clipBehavior: Clip.antiAlias, child: Container( @@ -196,51 +202,56 @@ class _HomeRecentCardState extends State { ), child: Row( children: [ - Container( - margin: const EdgeInsets.all(10), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - clipBehavior: Clip.antiAlias, - child: CachedNetworkImage( - imageUrl: widget.history.cover, - width: 130, - height: double.infinity, - fit: BoxFit.cover, + if (!noCover) + Container( + margin: const EdgeInsets.only(left: 10, bottom: 10, top: 10), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + clipBehavior: Clip.antiAlias, + child: CachedNetworkImage( + imageUrl: widget.history.cover!, + width: 130, + height: double.infinity, + fit: BoxFit.cover, + ), ), - ), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - widget.history.title, - style: const TextStyle(color: Colors.white), - overflow: TextOverflow.ellipsis, - ), - Text( - FlutterI18n.translate( - context, - "home.watched", - translationParams: { - "ep": widget.history.episodeTitle, - }, - ), - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - if (_update.isNotEmpty) ...[ - const SizedBox(height: 8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ Text( - _update, + widget.history.title, style: const TextStyle(color: Colors.white), + overflow: TextOverflow.ellipsis, + ), + Text( + FlutterI18n.translate( + context, + "home.watched", + translationParams: { + "ep": widget.history.episodeTitle, + }, + ), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + maxLines: 1, ), + if (_update.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + _update, + style: const TextStyle(color: Colors.white), + ), + ], + const SizedBox(height: 16), ], - const SizedBox(height: 16), - ], + ), ), ), ], diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 98efc60a..78f35487 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -11,10 +11,10 @@ class SearchPageController extends GetxController { String _randomKey = ""; int get finishCount => searchResultList.where((element) => element.completed).length; + bool needRefresh = true; @override void onInit() { - getRuntime(); ever(search, (callback) { _randomKey = DateTime.now().millisecondsSinceEpoch.toString(); getResult(_randomKey); @@ -37,6 +37,7 @@ class SearchPageController extends GetxController { searchResultList.add(SearchResult(runitme: element)); } getResult(_randomKey); + needRefresh = false; } Future getResult(String key) async { diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 161da6ae..be643e7b 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -23,6 +23,9 @@ class _SearchPageState extends State { @override void initState() { c = Get.put(SearchPageController()); + if (c.needRefresh) { + c.getRuntime(); + } super.initState(); } diff --git a/lib/pages/settings/view.dart b/lib/pages/settings/view.dart index cdc6de7a..c2282e99 100644 --- a/lib/pages/settings/view.dart +++ b/lib/pages/settings/view.dart @@ -223,11 +223,17 @@ class _SettingsPageState extends State { desktopWidget: Icon(fluent.FluentIcons.color, size: 24), ), title: 'settings.theme'.i18n, - itemNameValue: { - 'settings.theme-system'.i18n: 'system', - 'settings.theme-light'.i18n: 'light', - 'settings.theme-dark'.i18n: 'dark', - }, + itemNameValue: () { + final map = { + 'settings.theme-system'.i18n: 'system', + 'settings.theme-light'.i18n: 'light', + 'settings.theme-dark'.i18n: 'dark', + }; + if (Platform.isAndroid) { + map['settings.theme-black'.i18n] = 'black'; + } + return map; + }(), buildSubtitle: () => 'settings.theme-subtitle'.i18n, applyValue: (value) { Get.find().changeTheme(value); diff --git a/lib/pages/watch/reader_controller.dart b/lib/pages/watch/reader_controller.dart index db8a8dc5..aded5903 100644 --- a/lib/pages/watch/reader_controller.dart +++ b/lib/pages/watch/reader_controller.dart @@ -14,7 +14,7 @@ class ReaderController extends GetxController { final int playIndex; final int episodeGroupId; final ExtensionRuntime runtime; - final String cover; + final String? cover; ReaderController({ required this.title, @@ -23,7 +23,7 @@ class ReaderController extends GetxController { required this.playIndex, required this.episodeGroupId, required this.runtime, - required this.cover, + this.cover, }); late Rx watchData = Rx(null); diff --git a/lib/pages/watch/video_controller.dart b/lib/pages/watch/video_controller.dart index 0b0a14d1..1b285b19 100644 --- a/lib/pages/watch/video_controller.dart +++ b/lib/pages/watch/video_controller.dart @@ -1,9 +1,12 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:get/get.dart'; @@ -11,6 +14,7 @@ import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:miru_app/api/bt_server.dart'; import 'package:miru_app/models/index.dart'; +import 'package:miru_app/pages/bt_dialog/view.dart'; import 'package:miru_app/pages/home/controller.dart'; import 'package:miru_app/pages/main/controller.dart'; import 'package:miru_app/router/router.dart'; @@ -22,6 +26,8 @@ import 'package:miru_app/utils/layout.dart'; import 'package:miru_app/utils/miru_directory.dart'; import 'package:window_manager/window_manager.dart'; import 'package:path/path.dart' as path; +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:crypto/crypto.dart'; class VideoPlayerController extends GetxController { final String title; @@ -64,6 +70,8 @@ class VideoPlayerController extends GetxController { String _torrenHash = ""; + // 复制当前 context + @override void onInit() { if (Platform.isAndroid) { @@ -115,7 +123,7 @@ class VideoPlayerController extends GetxController { Message( Text( FlutterI18n.translate( - cuurentContext, + currentContext, "video.subtitle-change", translationParams: {"title": value.files.first.name}, ), @@ -132,7 +140,7 @@ class VideoPlayerController extends GetxController { Message( Text( FlutterI18n.translate( - cuurentContext, + currentContext, "video.subtitle-change", translationParams: {"title": subtitles[callback].title}, ), @@ -182,16 +190,21 @@ class VideoPlayerController extends GetxController { } play() async { + // 如果已经 delete 当前 controller + if (!Get.isRegistered(tag: title)) { + return; + } + try { subtitles.clear(); selectedSubtitle.value = -1; final playUrl = playList[index.value].url; final watchData = await runtime.watch(playUrl) as ExtensionBangumiWatch; + if (watchData.type == ExtensionWatchBangumiType.torrent) { if (Get.find().btServerisRunning.value == false) { await BTServerUtils.startServer(); } - sendMessage( Message( Text('video.torrent-downloading'.i18n), @@ -225,9 +238,31 @@ class VideoPlayerController extends GetxController { playTorrentFile(torrentMediaFileList.first); } else { await player.open(Media(watchData.url, httpHeaders: watchData.headers)); + if (watchData.audioTrack != null) { + await player.setAudioTrack(AudioTrack.uri(watchData.audioTrack!)); + } } subtitles.addAll(watchData.subtitles ?? []); } catch (e) { + if (e is StartServerException) { + if (Platform.isAndroid) { + await showDialog( + context: currentContext, + builder: (context) => const BTDialog(), + ); + } else { + await fluent.showDialog( + context: currentContext, + builder: (context) => const BTDialog(), + ); + } + + // 延时 3 秒再重试 + await Future.delayed(const Duration(seconds: 3)); + + play(); + return; + } sendMessage( Message( Text(e.toString()), @@ -240,6 +275,7 @@ class VideoPlayerController extends GetxController { playTorrentFile(String file) { currentTorrentFile.value = file; + (player.platform as NativePlayer).setProperty("network-timeout", "60"); player.open(Media('${BTServerApi.baseApi}/torrent/$_torrenHash/$file')); } @@ -250,7 +286,7 @@ class VideoPlayerController extends GetxController { onExit() async { if (_torrenHash.isNotEmpty) { - await BTServerApi.removeTorrent(_torrenHash); + BTServerApi.removeTorrent(_torrenHash); } if (player.state.duration.inSeconds == 0) { @@ -262,7 +298,8 @@ class VideoPlayerController extends GetxController { Directory(coverDir).createSync(recursive: true); final epName = playList[index.value].name; final filename = '${title}_$epName'; - final file = File(path.join(coverDir, filename)); + final file = File( + path.join(coverDir, md5.convert(utf8.encode(filename)).toString())); if (file.existsSync()) { file.deleteSync(recursive: true); } @@ -270,7 +307,7 @@ class VideoPlayerController extends GetxController { player.screenshot().then((value) { file.writeAsBytes(value!).then( (value) async { - debugPrint("save.."); + debugPrint("save.. ${value.path}"); await DatabaseUtils.putHistory( History() ..url = detailUrl diff --git a/lib/pages/watch/view.dart b/lib/pages/watch/view.dart index ce6e7895..2894ca00 100644 --- a/lib/pages/watch/view.dart +++ b/lib/pages/watch/view.dart @@ -14,7 +14,7 @@ class WatchPage extends StatelessWidget { required this.playerIndex, required this.episodeGroupId, required this.detailUrl, - required this.cover, + this.cover, }) : super(key: key); final List playList; final int playerIndex; @@ -22,7 +22,7 @@ class WatchPage extends StatelessWidget { final String package; final String detailUrl; final int episodeGroupId; - final String cover; + final String? cover; @override Widget build(BuildContext context) { diff --git a/lib/pages/watch/widgets/reader/comic/comic_reader.dart b/lib/pages/watch/widgets/reader/comic/comic_reader.dart index cfed2241..9a727847 100644 --- a/lib/pages/watch/widgets/reader/comic/comic_reader.dart +++ b/lib/pages/watch/widgets/reader/comic/comic_reader.dart @@ -17,7 +17,7 @@ class ComicReader extends StatefulWidget { required this.playerIndex, required this.episodeGroupId, required this.runtime, - required this.cover, + this.cover, }) : super(key: key); final String title; @@ -26,7 +26,7 @@ class ComicReader extends StatefulWidget { final int playerIndex; final int episodeGroupId; final ExtensionRuntime runtime; - final String cover; + final String? cover; @override State createState() => _ComicReaderState(); diff --git a/lib/pages/watch/widgets/reader/novel/novel_reader.dart b/lib/pages/watch/widgets/reader/novel/novel_reader.dart index a388b7b9..b6e6acd2 100644 --- a/lib/pages/watch/widgets/reader/novel/novel_reader.dart +++ b/lib/pages/watch/widgets/reader/novel/novel_reader.dart @@ -16,7 +16,7 @@ class NovelReader extends StatefulWidget { required this.playerIndex, required this.title, required this.detailUrl, - required this.cover, + this.cover, }) : super(key: key); final String title; @@ -25,7 +25,7 @@ class NovelReader extends StatefulWidget { final int playerIndex; final int episodeGroupId; final ExtensionRuntime runtime; - final String cover; + final String? cover; @override State createState() => _NovelReaderState(); diff --git a/lib/router/router.dart b/lib/router/router.dart index 15efa3ee..3f87192d 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -19,7 +19,7 @@ import 'package:miru_app/pages/settings/view.dart'; final rootNavigatorKey = GlobalKey(); final _shellNavigatorKey = GlobalKey(); -BuildContext get cuurentContext { +BuildContext get currentContext { if (Platform.isAndroid) { return Get.context!; } diff --git a/lib/utils/bt_server.dart b/lib/utils/bt_server.dart index c6a8dbf3..5e46f0df 100644 --- a/lib/utils/bt_server.dart +++ b/lib/utils/bt_server.dart @@ -28,30 +28,30 @@ class BTServerUtils { final res = dio.get(url); final remoteVersion = (await res).data["tag_name"] as String; debugPrint("最新版本: $remoteVersion"); - late String structure; + late String arch; late String platform; if (Platform.isAndroid) { final supportedAbis = androidDeviceInfo.supportedAbis; if (supportedAbis.contains("armeabi-v7a")) { - structure = "arm"; + arch = "arm"; } if (supportedAbis.contains("x86_64")) { - structure = "amd64"; + arch = "amd64"; } if (supportedAbis.contains("arm64-v8a")) { - structure = "arm64"; + arch = "arm64"; } platform = "android"; } if (Platform.isWindows) { - structure = "amd64.exe"; + arch = "amd64.exe"; platform = "windows"; } - debugPrint("下载 bt-server $remoteVersion $platform $structure"); + debugPrint("下载 bt-server $remoteVersion $platform $arch"); final downloadUrl = - "https://github.com/miru-project/bt-server/releases/download/$remoteVersion/bt-server-$remoteVersion-$platform-$structure"; + "https://github.com/miru-project/bt-server/releases/download/$remoteVersion/bt-server-$remoteVersion-$platform-$arch"; final savePath = await MiruDirectory.getDirectory; await dio.download( @@ -75,7 +75,7 @@ class BTServerUtils { try { if (Platform.isWindows) { - await Process.run( + _process = await Process.start( btServerPath, [], workingDirectory: savePath, @@ -93,9 +93,11 @@ class BTServerUtils { final error = e.toString(); if (error.contains("cannot find the file") || error.contains("No such file or directory")) { - Get.find().isInstalled.value = false; + if (Get.isRegistered()) { + Get.find().isInstalled.value = false; + } } - rethrow; + throw StartServerException('Start bt-server failed'); } checkServer(); } @@ -120,6 +122,27 @@ class BTServerUtils { }); } + // 检查更新 + static Future getRemoteVersion() async { + try { + const url = + "https://api.github.com/repos/miru-project/bt-server/releases/latest"; + final res = Dio().get(url); + final remoteVersion = (await res).data["tag_name"] as String; + return remoteVersion.replaceFirst("v", ''); + } catch (e) { + return Get.find().btServerVersion.value; + } + } + + // 卸载 bt-server + static Future uninstall() async { + stopServer(); + final savePath = await MiruDirectory.getDirectory; + final btServerPath = path.join(savePath, _getBTServerFilename()); + await File(btServerPath).delete(); + } + static Future isInstalled() async { final savePath = await MiruDirectory.getDirectory; final btServerPath = path.join(savePath, _getBTServerFilename()); @@ -134,3 +157,12 @@ class BTServerUtils { return "btserver"; } } + +class StartServerException implements Exception { + final String message; + StartServerException(this.message); + @override + String toString() { + return message; + } +} diff --git a/lib/utils/color.dart b/lib/utils/color.dart new file mode 100644 index 00000000..a1a9afc2 --- /dev/null +++ b/lib/utils/color.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class ColorUtils { + static Color getColor(String text) { + final int colorIndex = text.length % 10; + final color = [ + Colors.blueGrey[500], + Colors.brown[500], + Colors.deepPurple[500], + Colors.green[500], + Colors.indigo[500], + Colors.lightBlue[500], + Colors.lightGreen[500], + Colors.orange[500], + Colors.pink[500], + Colors.purple[500], + Colors.red[500], + Colors.teal[500], + ][colorIndex]; + return color!; + } +} diff --git a/lib/utils/database.dart b/lib/utils/database.dart index 8cabe380..5e6b1f43 100644 --- a/lib/utils/database.dart +++ b/lib/utils/database.dart @@ -12,8 +12,8 @@ class DatabaseUtils { static toggleFavorite({ required String package, required String url, - required String cover, required String name, + String? cover, }) async { return db.writeTxn(() async { if (await isFavorite( diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index a8a92b8a..b1bb311e 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -20,6 +21,7 @@ import 'package:path/path.dart' as path; class ExtensionUtils { static late Map runtimes; static late Map extensionErrorMap; + static Timer? _timer; static Future get getExtensionsDir async => path.join(await MiruDirectory.getDirectory, 'extensions'); @@ -31,7 +33,11 @@ class ExtensionUtils { await _loadExtensions(); // 监听目录变化 Directory(await getExtensionsDir).watch().listen((event) { - _loadExtensions(); + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () async { + await _loadExtensions(); + debugPrint("load extension"); + }); }); } @@ -67,7 +73,7 @@ class ExtensionUtils { } // 重载搜索页面 if (Get.isRegistered()) { - Get.find().getRuntime(); + Get.find().needRefresh = true; } } diff --git a/lib/utils/layout.dart b/lib/utils/layout.dart index 13444230..c02e00aa 100644 --- a/lib/utils/layout.dart +++ b/lib/utils/layout.dart @@ -6,12 +6,12 @@ class LayoutUtils { // 获取当前宽度 static double get getWidth { - return MediaQuery.of(cuurentContext).size.width; + return MediaQuery.of(currentContext).size.width; } // 获取当前高度 static double get getHeight { - return MediaQuery.of(cuurentContext).size.height; + return MediaQuery.of(currentContext).size.height; } // 是否是平板 diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index 6e7c13c5..ea574027 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -112,6 +112,7 @@ class MiruStorage { await _initSetting(SettingKey.theme, 'system'); await _initSetting(SettingKey.enableNSFW, false); await _initSetting(SettingKey.videoPlayer, 'built-in'); + await _initSetting(SettingKey.listMode, "grid"); } static _initSetting(String key, dynamic value) async { @@ -139,4 +140,5 @@ class SettingKey { static String enableNSFW = 'EnableNSFW'; static String videoPlayer = 'VideoPlayer'; static String databaseVersion = 'DatabaseVersion'; + static String listMode = 'ListMode'; } diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 6e07bafb..be582997 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -121,6 +121,15 @@ class PlatformToggleButton extends fluent.StatelessWidget { Widget _buildAndroid(BuildContext context) { return TextButton( onPressed: () => onChanged?.call(!checked), + style: ButtonStyle( + side: MaterialStateProperty.all( + BorderSide( + color: checked + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + ), + ), + ), child: Text( text, style: TextStyle( diff --git a/lib/widgets/card_tile.dart b/lib/widgets/card_tile.dart index a74cb23f..366166e0 100644 --- a/lib/widgets/card_tile.dart +++ b/lib/widgets/card_tile.dart @@ -6,8 +6,10 @@ class CardTile extends StatelessWidget { required this.title, required this.child, this.trailing, + this.leading, }) : super(key: key); final String title; + final Widget? leading; final Widget? trailing; final Widget child; @@ -34,6 +36,8 @@ class CardTile extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + const SizedBox(width: 3), + if (leading != null) leading!, const Spacer(), if (trailing != null) trailing! ], diff --git a/lib/widgets/cover.dart b/lib/widgets/cover.dart new file mode 100644 index 00000000..e01545db --- /dev/null +++ b/lib/widgets/cover.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:miru_app/utils/color.dart'; +import 'package:miru_app/widgets/cache_network_image.dart'; + +class Cover extends StatelessWidget { + const Cover({ + Key? key, + required this.alt, + this.url, + this.noText = false, + }) : super(key: key); + final String? url; + final String alt; + final bool noText; + + @override + Widget build(BuildContext context) { + if (url != null) { + return CacheNetWorkImage( + url!, + width: double.infinity, + height: double.infinity, + ); + } + + return Container( + padding: const EdgeInsets.all(8), + color: ColorUtils.getColor(alt), + child: noText + ? const SizedBox.expand() + : Center( + child: Text( + alt, + style: const TextStyle( + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + maxLines: 6, + ), + ), + ); + } +} diff --git a/lib/widgets/extension_item_card.dart b/lib/widgets/extension_item_card.dart index aba48f19..4dc3f8bc 100644 --- a/lib/widgets/extension_item_card.dart +++ b/lib/widgets/extension_item_card.dart @@ -11,11 +11,11 @@ class ExtensionItemCard extends StatefulWidget { required this.title, required this.url, required this.package, - required this.cover, + this.cover, this.update, }) : super(key: key); final String title; - final String cover; + final String? cover; final String? update; final String url; final String package; @@ -36,7 +36,7 @@ class _ExtensionItemCardState extends State { Get.to(DetailPage( url: widget.url, package: widget.package, - heroTag: widget.url, + tag: widget.url, )); }, ), diff --git a/lib/widgets/grid_item_tile.dart b/lib/widgets/grid_item_tile.dart index 48a0c8b7..e978ff34 100644 --- a/lib/widgets/grid_item_tile.dart +++ b/lib/widgets/grid_item_tile.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:miru_app/widgets/cache_network_image.dart'; +import 'package:miru_app/widgets/cover.dart'; import 'package:miru_app/widgets/platform_widget.dart'; class GridItemTile extends StatefulWidget { const GridItemTile({ Key? key, required this.title, - required this.cover, + this.cover, this.subtitle, this.onTap, }) : super(key: key); final String title; - final String cover; + final String? cover; final String? subtitle; final Function()? onTap; @@ -30,10 +30,9 @@ class _GridItemTileState extends State { borderRadius: BorderRadius.all(Radius.circular(8)), ), clipBehavior: Clip.antiAlias, - child: CacheNetWorkImage( - widget.cover, - width: double.infinity, - height: double.infinity, + child: Cover( + alt: widget.title, + url: widget.cover, ), ), Positioned( @@ -132,9 +131,9 @@ class _GridItemTileState extends State { child: AnimatedScale( scale: _isHover ? 1.05 : 1, duration: const Duration(milliseconds: 80), - child: CacheNetWorkImage( - widget.cover, - width: double.infinity, + child: Cover( + alt: widget.title, + url: widget.cover, ), )), ), diff --git a/pubspec.lock b/pubspec.lock index dfb8fb70..ee97b520 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "20071638cbe4e5964a427cfa0e86dce55d060bc7d82d56f3554095d7239a8765" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.4.2" args: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" build_runner: dependency: "direct dev" description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.6.3" cached_network_image: dependency: "direct main" description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.6.0" collection: dependency: transitive description: @@ -234,7 +234,7 @@ packages: source: hosted version: "1.6.3" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -285,10 +285,10 @@ packages: dependency: "direct main" description: name: desktop_webview_window - sha256: a2a902307f2fc588814c260a7dc6f0b7908499e43ca71d8f22fc48a39864ea54 + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.3" device_info_plus: dependency: "direct main" description: @@ -309,10 +309,10 @@ packages: dependency: "direct main" description: name: dio - sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 + sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7" url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.3.3" dio_cookie_manager: dependency: "direct main" description: @@ -365,18 +365,18 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: name: file_picker - sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 url: "https://pub.dev" source: hosted - version: "5.3.3" + version: "5.5.0" fixnum: dependency: transitive description: @@ -389,10 +389,10 @@ packages: dependency: "direct main" description: name: fluent_ui - sha256: "0ec5ae618b42e6b9d0449d7519dec28c201809bc1955c5effb5a211dd77fa30e" + sha256: b015021e7b19107ebf06277531d48fa40e450a500bad751a330ab5f94c815650 url: "https://pub.dev" source: hosted - version: "4.7.3" + version: "4.7.4" flutter: dependency: "direct main" description: flutter @@ -466,10 +466,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_localizations: dependency: transitive description: flutter @@ -479,18 +479,18 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431" + sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f url: "https://pub.dev" source: hosted - version: "0.6.17+1" + version: "0.6.17+3" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -513,10 +513,10 @@ packages: dependency: "direct main" description: name: get - sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e url: "https://pub.dev" source: hosted - version: "4.6.5" + version: "4.6.6" glob: dependency: transitive description: @@ -529,10 +529,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b3cadd2cd59a4103fd5f6bc572ca75111264698784e927aa471921c3477d5475 + sha256: e1a30a66d734f9e498b1b6522d6a75ded28242bad2359a9158df38a1c30bcf1f url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.2.0" graphs: dependency: transitive description: @@ -601,10 +601,10 @@ packages: dependency: transitive description: name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + sha256: "6e703d5e2f8c63fb31a77753915c1ec8baebde8088844e0d29f71b8f0b108888" url: "https://pub.dev" source: hosted - version: "4.0.17" + version: "4.1.0" intl: dependency: transitive description: @@ -729,26 +729,26 @@ packages: dependency: "direct main" description: name: media_kit - sha256: "0a89e7037002a62701ec319c375586849f9ef8e681820e1dd4a4ff7b843f7542" + sha256: "92c7f59e075d74471b31e703f81ccc1d7102739ebcce945b30a6417fa2f751d5" url: "https://pub.dev" source: hosted - version: "1.1.4+1" + version: "1.1.7" media_kit_libs_android_video: dependency: "direct main" description: name: media_kit_libs_android_video - sha256: "142d389bf3efcf8469594a9c7a06a92fc25843fc6c0c3247f76cdcf70b3b29de" + sha256: "498a5062bc5f000bd23ada3be788ea886ab32c52f7a8252dde1264ca019b819b" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" media_kit_libs_linux: dependency: "direct main" description: name: media_kit_libs_linux - sha256: "570bf18ebbd1221caec082657468be05d180510385d3515ec38e0be44fdcc859" + sha256: "3b7c272179639a914dc8a50bf8a3f2df0e9a503bd727c88fab499dbdf6cb1eb8" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" media_kit_libs_macos_video: dependency: "direct main" description: @@ -761,10 +761,10 @@ packages: dependency: "direct main" description: name: media_kit_libs_windows_video - sha256: f33aabd8414470d99e2c91dd98d605e6a5f1c4b8082dd933c10951bc961b9124 + sha256: "923f068344d7d200184e0aaa2597f3de6c05982a3b1f18035d842ab53f2a1350" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.0.8" media_kit_native_event_loop: dependency: "direct main" description: @@ -777,10 +777,10 @@ packages: dependency: "direct main" description: name: media_kit_video - sha256: e7fcbe426d42a78ad6696f8f557adb9cbdc012177829026d04992cc106a1c815 + sha256: cd3ab78e7626146f115134b82c4029ac5987ba6351719c9067d86789723e0c12 url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.8" meta: dependency: transitive description: @@ -849,10 +849,10 @@ packages: dependency: "direct main" description: name: palette_generator - sha256: "0e3cd6974e10b1434dcf4cf779efddb80e2696585e273a2dbede6af52f94568d" + sha256: eb7082b4b97487ebc65b3ad3f6f0b7489b96e76840381ed0e06a46fe7ffd4068 url: "https://pub.dev" source: hosted - version: "0.3.3+2" + version: "0.3.3+3" path: dependency: "direct main" description: @@ -881,50 +881,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" petitparser: dependency: transitive description: @@ -937,18 +937,18 @@ packages: dependency: transitive description: name: platform - sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pointycastle: dependency: transitive description: @@ -1017,10 +1017,10 @@ packages: dependency: transitive description: name: screen_brightness - sha256: "62fd61a64e68b32b98b840bad7d8b6822bbc40e63c2b569a5f85528484c86b41" + sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.2+1" screen_brightness_android: dependency: transitive description: @@ -1057,18 +1057,18 @@ packages: dependency: transitive description: name: screen_brightness_windows - sha256: "80d90ecdc63fc0823f2ecb1be323471619287937e14210650d7b25ca181abd05" + sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3" screen_retriever: dependency: transitive description: name: screen_retriever - sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323" + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.9" screenshot: dependency: "direct main" description: @@ -1342,66 +1342,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.1.14" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" + sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 url: "https://pub.dev" source: hosted - version: "6.0.38" + version: "6.1.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.0.20" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.8" uuid: dependency: transitive description: @@ -1422,10 +1422,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.9.0" + version: "11.10.0" volume_controller: dependency: transitive description: @@ -1478,42 +1478,42 @@ packages: dependency: transitive description: name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" win32: dependency: transitive description: name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + sha256: c97defd418eef4ec88c0d1652cdce84b9f7b63dd7198e266d06ac1710d527067 url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.0.8" win32_registry: dependency: transitive description: name: win32_registry - sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" window_manager: dependency: "direct main" description: name: window_manager - sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88" + sha256: "6ee795be9124f90660ea9d05e581a466de19e1c89ee74fc4bf528f60c8600edd" url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.3.6" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" xml: dependency: transitive description: @@ -1563,5 +1563,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 248d396b..63a6cb2c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: miru_app description: A new Flutter project. publish_to: "none" -version: 1.7.0+35 +version: 1.7.1+36 environment: sdk: ">=3.0.3 <4.0.0" @@ -12,7 +12,7 @@ dependencies: dio: ^5.3.2 dio_cookie_manager: ^3.1.0+1 easy_refresh: ^3.3.2+1 - fluent_ui: ^4.7.3 + fluent_ui: ^4.7.4 flutter: sdk: flutter flutter_animate: ^4.1.1+1 @@ -29,11 +29,11 @@ dependencies: isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 json_annotation: ^4.8.1 - media_kit: ^1.1.4+1 - media_kit_libs_android_video: ^1.3.2 - media_kit_libs_linux: ^1.1.1 + media_kit: ^1.1.7 + media_kit_libs_android_video: ^1.3.3 + media_kit_libs_linux: ^1.1.2 media_kit_libs_macos_video: ^1.1.3 - media_kit_libs_windows_video: ^1.0.7 + media_kit_libs_windows_video: ^1.0.8 media_kit_native_event_loop: ^1.0.7 media_kit_video: ^1.1.5 package_info_plus: ^4.1.0 @@ -54,6 +54,7 @@ dependencies: xpath_selector_html_parser: ^3.0.1 device_info_plus: ^9.0.3 android_intent_plus: ^4.0.2 + crypto: ^3.0.3 dev_dependencies: