Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions violet/lib/database/user/bookmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,36 @@ class BookmarkCropImage {
int page() => result['Page'];
double aspectRatio() => result['AspectRatio'];
String area() => result['Area'];
String userId() => result['UserId'];

Future<void> update() async {
var db = await CommonUserDatabase.getInstance();
await db.update('BookmarkCropImage', result, 'Id=?', [id()]);
}

factory BookmarkCropImage.fromJson(Map<String, dynamic> json) {
return BookmarkCropImage(result: {
'Article': json['article'],
'Page': json['page'],
'Area': json['area'],
'AspectRatio': json['aspectRatio'],
'DateTime': json['datetime'],
if (json.containsKey('user_id')) 'UserId': json['user_id'],
});
}

static List<BookmarkCropImage> fromJsonList(List<dynamic> jsonData) {
return jsonData
.map((e) => BookmarkCropImage.fromJson(e as Map<String, dynamic>))
.toList();
}

Map<String, dynamic> toJson() => {
'article': article(),
'page': page(),
'aspectRatio': aspectRatio(),
'area': area(),
// datetime and user_id are not included in the json
};
}

Expand Down
79 changes: 68 additions & 11 deletions violet/lib/pages/bookmark/crop_bookmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pull_down_button/pull_down_button.dart';
import 'package:violet/database/query.dart';
import 'package:violet/database/user/bookmark.dart';
import 'package:violet/log/log.dart';
import 'package:violet/other/dialogs.dart';
import 'package:violet/pages/bookmark/crop_id_rank_page.dart';
import 'package:violet/pages/common/toast.dart';
import 'package:violet/pages/common/utils.dart';
import 'package:violet/pages/segment/platform_navigator.dart';
Expand Down Expand Up @@ -326,17 +328,8 @@ class _CropBookmarkPageState extends State<CropBookmarkPage> {
file.writeContent(outputStream);

final json = jsonDecode(utf8.decode(outputStream.getBytes()));
List<BookmarkCropImage> bookmarks = [];
for (final e in json as List<dynamic>) {
final bookmark = BookmarkCropImage(result: {
'Article': e['article'],
'Page': e['page'],
'Area': e['area'],
'AspectRatio': e['aspectRatio'],
'DateTime': e['datetime'],
});
bookmarks.add(bookmark);
}
final bookmarks =
BookmarkCropImage.fromJsonList(json as List<dynamic>);

if (!context.mounted) return;
PlatformNavigator.navigateSlide(
Expand All @@ -351,6 +344,70 @@ class _CropBookmarkPageState extends State<CropBookmarkPage> {
}
},
),
PullDownMenuItem(
title: 'User Bookmarks (with Id)',
icon: CupertinoIcons.bookmark,
onTap: () async {
final jsons = json.decode(await rootBundle.loadString(
'assets/crop-bookmarks-user.json')) as List<dynamic>;
final bookmarks = BookmarkCropImage.fromJsonList(jsons);

if (!context.mounted) return;
PlatformNavigator.navigateSlide(
context,
CropBookmarkPage(
bookmarks: bookmarks
.sortedBy((e) => DateTime.parse(e.datetime()))
.toList()),
opaque: false,
);
},
),
PullDownMenuItem(
title: 'User Bookmarks (with Id Rank)',
icon: CupertinoIcons.bookmark,
onTap: () async {
if (!context.mounted) return;
PlatformNavigator.navigateSlide(
context,
CropIdRankPage(),
opaque: false,
);
},
),
PullDownMenuItem(
title: 'User Bookmarks (with Custom Tag)',
icon: CupertinoIcons.bookmark,
onTap: () async {
final jsons = json.decode(await rootBundle.loadString(
'assets/crop-bookmarks-user.json')) as List<dynamic>;
final bookmarks = BookmarkCropImage.fromJsonList(jsons);

final queryRaw = await QueryManager.queryIds(
bookmarks.map((e) => e.article()).toList());
final remainIds = queryRaw
.where((e) => e.tags() != null)
.where((e) => (e.tags() as String)
.split('|')
.contains('female:sole female'))
.map((e) => e.id())
.toSet();

final remainBookmarks = bookmarks
.where((e) => remainIds.contains(e.article()))
.toList();

if (!context.mounted) return;
PlatformNavigator.navigateSlide(
context,
CropBookmarkPage(
bookmarks: remainBookmarks
.sortedBy((e) => DateTime.parse(e.datetime()))
.toList()),
opaque: false,
);
},
),
// TODO: enable select mode
// const PullDownMenuDivider.large(),
// PullDownMenuItem(
Expand Down
160 changes: 160 additions & 0 deletions violet/lib/pages/bookmark/crop_id_rank_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:violet/database/user/bookmark.dart';
import 'package:violet/pages/bookmark/crop_bookmark.dart'
show CropBookmarkPage, CropImageWidget;
import 'package:violet/pages/common/utils.dart' show getImageProviderFromId;

class CropIdRankPage extends StatefulWidget {
const CropIdRankPage({super.key});

@override
State<CropIdRankPage> createState() => _CropIdRankPageState();
}

class _UserCropRank {
final String userId;
final List<Map<String, dynamic>> crops;
const _UserCropRank({
required this.userId,
required this.crops,
});
}

class _CropIdRankPageState extends State<CropIdRankPage> {
late Future<List<_UserCropRank>> _futureRank;

@override
void initState() {
super.initState();
_futureRank = _loadRank();
}

Future<List<_UserCropRank>> _loadRank() async {
final jsonStr =
await rootBundle.loadString('assets/crop-bookmarks-user.json');
final List<dynamic> data = json.decode(jsonStr);
final Map<String, List<Map<String, dynamic>>> userMap = {};
for (final e in data) {
final userId = e['user_id'] as String?;
if (userId == null) continue;
userMap.putIfAbsent(userId, () => []).add(e as Map<String, dynamic>);
}
final list = userMap.entries
.map((e) => _UserCropRank(
userId: e.key,
crops: e.value,
))
.toList();
list.sort((a, b) => b.crops.length.compareTo(a.crops.length));
return list;
}

void _onUserTap(_UserCropRank user) {
final bookmarks = BookmarkCropImage.fromJsonList(user.crops);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CropBookmarkPage(bookmarks: bookmarks),
),
);
}

Widget _buildCropPreviews(List<Map<String, dynamic>> crops) {
return Row(
children: crops.take(6).map((crop) {
final articleId = crop['article'] as int;
final page = crop['page'] as int;
final area = (crop['area'] as String)
.split(',')
.map((e) => double.parse(e))
.toList();
final aspectRatio = (crop['aspectRatio'] as num).toDouble();
final rect = Rect.fromLTRB(area[0], area[1], area[2], area[3]);
return Expanded(
child: AspectRatio(
aspectRatio: 1,
child: FutureBuilder<({String url, Map<String, String> headers})>(
future: () async {
final provider = await getImageProviderFromId(articleId);
final url = await provider.getImageUrl(page);
final headers = await provider.getHeader(page);
return (url: url, headers: headers);
}(),
builder: (context, snap) {
if (!snap.hasData) {
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2)));
}
return ClipRRect(
borderRadius: BorderRadius.circular(6),
child: CropImageWidget(
articleId: articleId,
page: page,
url: snap.data!.url,
headers: snap.data!.headers,
rect: rect,
aspectRatio: aspectRatio,
columnCount: 1,
showOverlay: ValueNotifier(false),
),
);
},
),
),
);
}).toList(),
);
}

Widget _buildUserRankTile(_UserCropRank user, int idx) {
return InkWell(
onTap: () => _onUserTap(user),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('#${idx + 1}',
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
Expanded(child: Text(user.userId)),
Text('${user.crops.length}',
style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
_buildCropPreviews(user.crops),
],
),
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Crop Bookmark User Rank')),
body: FutureBuilder<List<_UserCropRank>>(
future: _futureRank,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final rankList = snapshot.data!;
return ListView.separated(
itemCount: rankList.length,
separatorBuilder: (context, idx) => const Divider(height: 1),
itemBuilder: (context, idx) =>
_buildUserRankTile(rankList[idx], idx),
);
},
),
);
}
}
2 changes: 2 additions & 0 deletions violet/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ flutter:

- assets/rank/similar_articles_with_scores.json

- assets/crop-bookmarks-user.json

# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.

Expand Down
10 changes: 6 additions & 4 deletions violet/script/daily-crop.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
query = """
SELECT
event_timestamp,
(SELECT param.value.string_value
FROM UNNEST(user_properties) AS param
WHERE param.key = 'user_id') as user_id,
(SELECT param.value.string_value
FROM UNNEST(event_params) AS param
WHERE param.key = 'Area') as area,
Expand All @@ -35,8 +38,6 @@
`real-violet-app.analytics_238885015.events_*`
WHERE
event_name = "bookmark_crop"
LIMIT
5000
"""

datas = []
Expand All @@ -47,6 +48,7 @@
"aspectRatio": row["aspect_ratio"],
"article": row["article"],
"page": row["page"],
"user_id": row["user_id"],
}
dedup = json.dumps(data)
if dedup not in dedups:
Expand All @@ -60,7 +62,7 @@

print(len(datas))

filename = "assets/daily/crop-bookmarks.json"
os.makedirs(os.path.dirname(filename), exist_ok=True)
filename = "crop-bookmarks-5.json"
# os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w") as file:
file.write(json.dumps(datas))
Loading